atproto relay in zig zlay.waow.tech
relay zig atproto
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803//! relay frame validator — DID key resolution + real signature verification//!//! validates firehose commit frames by verifying the commit signature against//! the pre-resolved signing key for the DID. accepts pre-decoded CBOR payload//! from the subscriber (decoded via zat SDK). on cache miss, skips validation//! and queues background resolution. no frame is ever blocked on network I/O.
const std = @import("std");const Io = std.Io;const zat = @import("zat");const broadcaster = @import("broadcaster.zig");const event_log_mod = @import("event_log.zig");const lru = @import("util/lru.zig");const util = @import("util/util.zig");const atproto = @import("atproto/main.zig");
const Allocator = std.mem.Allocator;const log = std.log.scoped(.relay);
const parseEnvInt = util.parseEnvInt;const timestamp = util.timestamp;const microTimestamp = util.microTimestamp;const extractHostFromUrl = atproto.extractHostFromUrl;
const HostAuthorityCache = struct { const Key = struct { did: []const u8, incoming_host_id: u64, };
const KeyContext = struct { pub fn hash(_: KeyContext, key: Key) u64 { var h = std.hash.Wyhash.init(0); h.update(key.did); h.update(std.mem.asBytes(&key.incoming_host_id)); return h.final(); }
pub fn eql(_: KeyContext, a: Key, b: Key) bool { return a.incoming_host_id == b.incoming_host_id and std.mem.eql(u8, a.did, b.did); } };
const Map = std.HashMapUnmanaged(Key, i64, KeyContext, 80); const recheck_interval_seconds: i64 = 60; const default_capacity: u32 = 8192;
allocator: Allocator, io: Io, map: Map = .empty, capacity: u32, mutex: Io.Mutex = Io.Mutex.init,
fn init(allocator: Allocator, io: Io, capacity: u32) HostAuthorityCache { return .{ .allocator = allocator, .io = io, .capacity = capacity, }; }
fn deinit(self: *HostAuthorityCache) void { self.clearLocked(); self.map.deinit(self.allocator); }
fn recentlyRejected(self: *HostAuthorityCache, did: []const u8, incoming_host_id: u64) bool { return self.recentlyRejectedAt(did, incoming_host_id, self.now()); }
fn recentlyRejectedAt(self: *HostAuthorityCache, did: []const u8, incoming_host_id: u64, current_time: i64) bool { self.mutex.lockUncancelable(self.io); defer self.mutex.unlock(self.io);
const key: Key = .{ .did = did, .incoming_host_id = incoming_host_id }; const checked_at = self.map.getContext(key, .{}) orelse return false; if (current_time >= checked_at and current_time - checked_at < recheck_interval_seconds) return true;
if (self.map.fetchRemoveContext(key, .{})) |removed| { self.allocator.free(removed.key.did); } return false; }
fn remember(self: *HostAuthorityCache, did: []const u8, incoming_host_id: u64) bool { return self.rememberAt(did, incoming_host_id, self.now()); }
/// Returns true only for the first confirmed mismatch in the current TTL /// window. Concurrent duplicate network checks update the same entry. fn rememberAt(self: *HostAuthorityCache, did: []const u8, incoming_host_id: u64, current_time: i64) bool { self.mutex.lockUncancelable(self.io); defer self.mutex.unlock(self.io);
const lookup: Key = .{ .did = did, .incoming_host_id = incoming_host_id }; if (self.map.getPtrContext(lookup, .{})) |checked_at| { checked_at.* = current_time; return false; }
if (self.map.count() >= self.capacity) { self.pruneExpiredLocked(current_time); if (self.map.count() >= self.capacity) self.clearLocked(); }
const owned_did = self.allocator.dupe(u8, did) catch return true; const owned_key: Key = .{ .did = owned_did, .incoming_host_id = incoming_host_id }; self.map.putContext(self.allocator, owned_key, current_time, .{}) catch { self.allocator.free(owned_did); return true; }; return true; }
fn forget(self: *HostAuthorityCache, did: []const u8, incoming_host_id: u64) void { self.mutex.lockUncancelable(self.io); defer self.mutex.unlock(self.io);
const key: Key = .{ .did = did, .incoming_host_id = incoming_host_id }; if (self.map.fetchRemoveContext(key, .{})) |removed| { self.allocator.free(removed.key.did); } }
fn count(self: *HostAuthorityCache) u32 { if (!self.mutex.tryLock()) return 0; defer self.mutex.unlock(self.io); return self.map.count(); }
fn pruneExpiredLocked(self: *HostAuthorityCache, current_time: i64) void { var expired: std.ArrayListUnmanaged(Key) = .empty; defer expired.deinit(self.allocator);
var it = self.map.iterator(); while (it.next()) |entry| { const checked_at = entry.value_ptr.*; if (current_time < checked_at or current_time - checked_at >= recheck_interval_seconds) { expired.append(self.allocator, entry.key_ptr.*) catch { self.clearLocked(); return; }; } }
for (expired.items) |key| { if (self.map.fetchRemoveContext(key, .{})) |removed| { self.allocator.free(removed.key.did); } } }
fn clearLocked(self: *HostAuthorityCache) void { var it = self.map.keyIterator(); while (it.next()) |key| self.allocator.free(key.did); self.map.clearRetainingCapacity(); }
fn now(self: *HostAuthorityCache) i64 { return Io.Timestamp.now(self.io, .awake).toSeconds(); }};
/// decoded and cached signing key for a DIDconst CachedKey = struct { key_type: zat.multicodec.KeyType, raw: [33]u8, // compressed public key (secp256k1 or p256) len: u8, resolve_time: i64 = 0, // epoch seconds when resolved};
pub const ValidationResult = struct { valid: bool, skipped: bool, // MST root CID from verified commit. only set by the verify_commit_diff // path — sig-only verification can't recover the MST root (verifyCommitCar // exposes the commit CID, which is a different block), so it returns null // rather than poison the prevData chain-continuity check downstream. data_cid: ?[]const u8 = null, commit_rev: ?[]const u8 = null, // rev from verified commit};
/// configuration for commit validation checkspub const ValidatorConfig = struct { /// verify MST structure during signature verification verify_mst: bool = false, // off by default for relay throughput /// verify commit diffs via MST inversion (sync 1.1) verify_commit_diff: bool = true, /// max allowed operations per commit max_ops: usize = 200, /// max clock skew for rev timestamps (seconds) rev_clock_skew: i64 = 300, // 5 minutes};
pub const Validator = struct { allocator: Allocator, stats: *broadcaster.Stats, config: ValidatorConfig, persist: ?*event_log_mod.DiskPersist = null, // DID → signing key cache (decoded, ready for verification) cache: lru.LruCache(CachedKey), // background resolve queue queue: std.ArrayListUnmanaged([]const u8) = .empty, // in-flight set — prevents duplicate DID entries in the queue queued_set: std.StringHashMapUnmanaged(void) = .empty, queue_mutex: Io.Mutex = Io.Mutex.init, queue_cond: Io.Condition = Io.Condition.init, resolver_futures: [max_resolver_threads]?Io.Future(void) = .{null} ** max_resolver_threads, alive: std.atomic.Value(bool) = .{ .raw = true }, max_cache_size: u32 = 250_000, io: Io, // pool of reusable resolvers for inline host authority and signing-key refreshes. // frame workers acquire/release via atomic flag to avoid creating // a fresh resolver (and fresh TLS handshake) per call. host_resolvers: [host_resolver_pool_size]zat.DidResolver = undefined, host_resolver_available: [host_resolver_pool_size]std.atomic.Value(bool) = .{std.atomic.Value(bool){ .raw = false }} ** host_resolver_pool_size, host_resolver_inited: bool = false, host_authority_cache: HostAuthorityCache,
const max_resolver_threads = 8; const default_resolver_threads = 4; const max_queue_size: usize = 100_000; const host_resolver_pool_size: usize = 4; const signature_refresh_min_interval: i64 = 60;
pub fn init(allocator: Allocator, stats: *broadcaster.Stats, io: Io) Validator { return initWithConfig(allocator, stats, .{}, io); }
pub fn initWithConfig(allocator: Allocator, stats: *broadcaster.Stats, config: ValidatorConfig, io: Io) Validator { return .{ .allocator = allocator, .stats = stats, .config = config, .cache = lru.LruCache(CachedKey).init(allocator, 250_000, io), .host_authority_cache = HostAuthorityCache.init(allocator, io, HostAuthorityCache.default_capacity), .io = io, }; }
pub fn deinit(self: *Validator) void { self.alive.store(false, .release); self.queue_cond.broadcast(self.io); for (&self.resolver_futures) |*slot| { if (slot.*) |*f| { f.cancel(self.io); } slot.* = null; }
if (self.host_resolver_inited) { for (&self.host_resolvers) |*r| { r.deinit(); } self.host_resolver_inited = false; }
self.cache.deinit(); self.host_authority_cache.deinit();
// free queued DIDs for (self.queue.items) |did| { self.allocator.free(did); } self.queue.deinit(self.allocator); self.queued_set.deinit(self.allocator); }
/// start background resolver threads and host authority resolver pool pub fn start(self: *Validator) !void { self.max_cache_size = parseEnvInt(u32, "VALIDATOR_CACHE_SIZE", self.max_cache_size); self.cache.capacity = self.max_cache_size; const n = parseEnvInt(u8, "RESOLVER_THREADS", default_resolver_threads); const count = @min(n, max_resolver_threads); for (self.resolver_futures[0..count]) |*slot| { slot.* = try self.io.concurrent(resolveLoop, .{self}); }
// init host authority resolver pool (reused across calls) for (&self.host_resolvers) |*r| { r.* = zat.DidResolver.initWithOptions(self.io, self.allocator, .{}); } for (&self.host_resolver_available) |*a| { a.store(true, .release); } self.host_resolver_inited = true; }
/// validate a #sync frame: signature verification only (no ops, no MST). /// #sync resets a repo to a new commit state — used for recovery from broken streams. /// on cache miss, queues background resolution and skips. pub fn validateSync(self: *Validator, payload: zat.cbor.Value) ValidationResult { const did = payload.getString("did") orelse { _ = self.stats.skipped.fetchAdd(1, .monotonic); return .{ .valid = true, .skipped = true }; };
if (zat.Did.parse(did) == null) { _ = self.stats.failed.fetchAdd(1, .monotonic); _ = self.stats.failed_bad_did.fetchAdd(1, .monotonic); return .{ .valid = false, .skipped = false }; }
// check rev is valid TID (if present) if (payload.getString("rev")) |rev| { if (zat.Tid.parse(rev) == null) { _ = self.stats.failed.fetchAdd(1, .monotonic); _ = self.stats.failed_bad_rev.fetchAdd(1, .monotonic); return .{ .valid = false, .skipped = false }; } }
const blocks = payload.getBytes("blocks") orelse { _ = self.stats.failed.fetchAdd(1, .monotonic); _ = self.stats.failed_missing_blocks.fetchAdd(1, .monotonic); return .{ .valid = false, .skipped = false }; };
// #sync CAR should be small (just the signed commit block) // lexicon maxLength: 10000 if (blocks.len > 10_000) { _ = self.stats.failed.fetchAdd(1, .monotonic); _ = self.stats.failed_oversized_blocks.fetchAdd(1, .monotonic); return .{ .valid = false, .skipped = false }; }
// cache lookup const cached_key: ?CachedKey = self.cache.get(did);
if (cached_key == null) { _ = self.stats.cache_misses.fetchAdd(1, .monotonic); _ = self.stats.skipped.fetchAdd(1, .monotonic); self.queueResolve(did); return .{ .valid = true, .skipped = true }; }
_ = self.stats.cache_hits.fetchAdd(1, .monotonic);
// verify signature (no MST, no ops) const public_key = zat.multicodec.PublicKey{ .key_type = cached_key.?.key_type, .raw = cached_key.?.raw[0..cached_key.?.len], };
var arena = std.heap.ArenaAllocator.init(self.allocator); defer arena.deinit();
const result = zat.verifyCommitCar(arena.allocator(), blocks, public_key, .{ .verify_mst = false, .expected_did = did, .max_car_size = 10 * 1024, }) catch |err| { log.debug("sync verification failed for {s}: {s}", .{ did, @errorName(err) }); if (err == error.SignatureVerificationFailed) { if (self.refreshSigningKey(did, cached_key.?)) |fresh_key| { const fresh_public_key = zat.multicodec.PublicKey{ .key_type = fresh_key.key_type, .raw = fresh_key.raw[0..fresh_key.len], }; const retry = zat.verifyCommitCar(arena.allocator(), blocks, fresh_public_key, .{ .verify_mst = false, .expected_did = did, .max_car_size = 10 * 1024, }) catch |retry_err| { log.debug("sync verification failed after key refresh for {s}: {s}", .{ did, @errorName(retry_err) }); return self.rejectSignature(); }; _ = self.stats.validated.fetchAdd(1, .monotonic); const loaded = zat.loadCommitFromCAR(arena.allocator(), blocks) catch return self.rejectCommitIntegrity(); return .{ .valid = true, .skipped = false, .data_cid = loaded.commit.data_cid, .commit_rev = retry.commit_rev, }; } return self.rejectSignature(); }
return self.rejectCommitIntegrity(); };
_ = self.stats.validated.fetchAdd(1, .monotonic); const loaded = zat.loadCommitFromCAR(arena.allocator(), blocks) catch return self.rejectCommitIntegrity(); return .{ .valid = true, .skipped = false, .data_cid = loaded.commit.data_cid, .commit_rev = result.commit_rev, }; }
/// validate a commit frame using a pre-decoded CBOR payload (from SDK decoder). /// on cache miss, queues background resolution and skips. pub fn validateCommit(self: *Validator, payload: zat.cbor.Value) ValidationResult { // extract DID from decoded payload const did = payload.getString("repo") orelse { _ = self.stats.skipped.fetchAdd(1, .monotonic); return .{ .valid = true, .skipped = true }; };
// check cache for pre-resolved signing key const cached_key: ?CachedKey = self.cache.get(did);
if (cached_key == null) { // cache miss — queue for background resolution, skip validation _ = self.stats.cache_misses.fetchAdd(1, .monotonic); _ = self.stats.skipped.fetchAdd(1, .monotonic); self.queueResolve(did); return .{ .valid = true, .skipped = true }; }
_ = self.stats.cache_hits.fetchAdd(1, .monotonic);
// cache hit — do structure checks + signature verification if (self.verifyCommit(payload, did, cached_key.?)) |vr| { _ = self.stats.validated.fetchAdd(1, .monotonic); return vr; } else |err| { log.debug("commit verification failed for {s}: {s}", .{ did, @errorName(err) }); if (err == error.SignatureVerificationFailed) { if (self.refreshSigningKey(did, cached_key.?)) |fresh_key| { if (self.verifyCommit(payload, did, fresh_key)) |vr| { _ = self.stats.validated.fetchAdd(1, .monotonic); return vr; } else |retry_err| { log.debug("commit verification failed after key refresh for {s}: {s}", .{ did, @errorName(retry_err) }); } } return self.rejectSignature(); }
switch (err) { error.InvalidCommitDiff, error.MstRootMismatch, error.PrevDataMismatch, error.InversionMismatch, error.PartialTree, error.DuplicatePath, error.InvalidMstNode, => _ = self.stats.failed_sync_1_1.fetchAdd(1, .monotonic), else => {}, }
return self.rejectCommitIntegrity(); } }
fn rejectSignature(self: *Validator) ValidationResult { _ = self.stats.failed.fetchAdd(1, .monotonic); _ = self.stats.failed_signature.fetchAdd(1, .monotonic); return .{ .valid = false, .skipped = false }; }
fn rejectCommitIntegrity(self: *Validator) ValidationResult { _ = self.stats.failed.fetchAdd(1, .monotonic); _ = self.stats.failed_commit_integrity.fetchAdd(1, .monotonic); return .{ .valid = false, .skipped = false }; }
/// Once a relay holds prior repo state, prevData is required to make the /// commit diff an inductive proof. A present-but-wrong value is rejected by /// MST inversion; this handles omission/null before the expensive verify. pub fn validatePrevDataPresence(self: *Validator, payload: zat.cbor.Value) bool { if (!self.config.verify_commit_diff) return true; if (payload.get("prevData")) |prev_data| { if (prev_data == .cid) return true; }
// counted apart from sync_1_1: an omitted prevData is a sender that // predates Sync 1.1, whereas a sync_1_1 failure means a proof did not // invert — which can equally mean our verifier is wrong. _ = self.stats.failed.fetchAdd(1, .monotonic); _ = self.stats.failed_prev_data_missing.fetchAdd(1, .monotonic); _ = self.stats.chain_breaks_prev_data.fetchAdd(1, .monotonic); return false; }
/// Refresh a signing key after a signature failure. A recently resolved key /// is already authoritative enough to reject against; this bounds directory /// traffic when a sender repeatedly supplies bad signatures. fn refreshSigningKey(self: *Validator, did: []const u8, cached_key: CachedKey) ?CachedKey { const now = timestamp(self.io); if (now - cached_key.resolve_time < signature_refresh_min_interval) return null; if (!self.host_resolver_inited) return null;
// Record the attempt before network I/O. If resolution fails, retain the // known key and wait before trying the directory again. var attempted_key = cached_key; attempted_key.resolve_time = now; self.cache.put(did, attempted_key) catch return null;
const parsed = zat.Did.parse(did) orelse return null; const idx = self.acquireHostResolver(); defer self.releaseHostResolver(idx);
var doc = self.host_resolvers[idx].resolve(parsed) catch return null; defer doc.deinit();
const vm = doc.signingKey() orelse return null; const key_bytes = zat.multibase.decode(self.allocator, vm.public_key_multibase) catch return null; defer self.allocator.free(key_bytes); const public_key = zat.multicodec.parsePublicKey(key_bytes) catch return null;
var fresh = CachedKey{ .key_type = public_key.key_type, .raw = undefined, .len = @intCast(public_key.raw.len), .resolve_time = now, }; @memcpy(fresh.raw[0..public_key.raw.len], public_key.raw); self.cache.put(did, fresh) catch return null; return fresh; }
fn verifyCommit(self: *Validator, payload: zat.cbor.Value, expected_did: []const u8, cached_key: CachedKey) !ValidationResult { // commit structure checks first (cheap, no allocation) self.checkCommitStructure(payload) catch { _ = self.stats.failed_bad_structure.fetchAdd(1, .monotonic); return error.InvalidFrame; };
// extract blocks (raw CAR bytes) from the pre-decoded payload const blocks = payload.getBytes("blocks") orelse return error.InvalidFrame;
// blocks size check — lexicon maxLength: 2000000 if (blocks.len > 2_000_000) return error.InvalidFrame;
// build public key for verification const public_key = zat.multicodec.PublicKey{ .key_type = cached_key.key_type, .raw = cached_key.raw[0..cached_key.len], };
// run real signature verification (needs its own arena for CAR/MST temporaries) var arena = std.heap.ArenaAllocator.init(self.allocator); defer arena.deinit(); const alloc = arena.allocator();
// Sync 1.1 path: validate the CAR diff against ops + prevData using // MST inversion. Do not silently fall back to signature-only validation // when the proof envelope is absent or malformed. if (self.config.verify_commit_diff) { const msg_ops = try self.extractOps(alloc, payload); const prev_data: ?[]const u8 = if (payload.get("prevData")) |pd| switch (pd) { .cid => |c| c.raw, .null => null, else => return error.InvalidFrame, } else null;
const diff_result = try zat.verifyCommitDiff(alloc, blocks, msg_ops, prev_data, public_key, .{ .expected_did = expected_did, });
return .{ .valid = true, .skipped = false, .data_cid = diff_result.data_cid, .commit_rev = diff_result.commit_rev, }; }
// fallback: legacy verification (signature + optional MST walk) const result = zat.verifyCommitCar(alloc, blocks, public_key, .{ .verify_mst = self.config.verify_mst, .expected_did = expected_did, }) catch |err| { return err; };
return .{ .valid = true, .skipped = false, .commit_rev = result.commit_rev, }; }
/// extract ops from payload and convert to mst.Operation array. /// the firehose format uses a single "path" field ("collection/rkey"), /// not separate "collection"/"rkey" fields. fn extractOps(self: *Validator, alloc: Allocator, payload: zat.cbor.Value) ![]const zat.MstOperation { _ = self; const ops_array = payload.getArray("ops") orelse return error.InvalidCommitDiff; var ops: std.ArrayListUnmanaged(zat.MstOperation) = .empty; for (ops_array) |op| { const action = op.getString("action") orelse return error.InvalidCommitDiff; const path = op.getString("path") orelse return error.InvalidCommitDiff;
// validate path contains "/" (collection/rkey) if (std.mem.indexOfScalar(u8, path, '/') == null) return error.InvalidCommitDiff;
// extract CID values const cid_value: ?[]const u8 = if (op.get("cid")) |v| switch (v) { .cid => |c| c.raw, else => null, } else null;
var value: ?[]const u8 = null; var prev: ?[]const u8 = null;
if (std.mem.eql(u8, action, "create")) { value = cid_value orelse return error.InvalidCommitDiff; } else if (std.mem.eql(u8, action, "update")) { value = cid_value orelse return error.InvalidCommitDiff; prev = if (op.get("prev")) |v| switch (v) { .cid => |c| c.raw, else => null, } else null; if (prev == null) return error.InvalidCommitDiff; } else if (std.mem.eql(u8, action, "delete")) { prev = if (op.get("prev")) |v| switch (v) { .cid => |c| c.raw, else => null, } else null; if (prev == null) return error.InvalidCommitDiff; } else return error.InvalidCommitDiff;
try ops.append(alloc, .{ .path = path, .value = value, .prev = prev, }); }
return ops.items; }
fn checkCommitStructure(self: *Validator, payload: zat.cbor.Value) !void { // check repo field is a valid DID const repo = payload.getString("repo") orelse return error.InvalidFrame; if (zat.Did.parse(repo) == null) return error.InvalidFrame;
// check rev is a valid TID if (payload.getString("rev")) |rev| { if (zat.Tid.parse(rev) == null) return error.InvalidFrame; }
// check ops count if (payload.get("ops")) |ops_value| { switch (ops_value) { .array => |ops| { if (ops.len > self.config.max_ops) return error.InvalidFrame; // validate each op has valid path (collection/rkey) for (ops) |op| { if (op.getString("path")) |path| { if (std.mem.indexOfScalar(u8, path, '/')) |sep| { const collection = path[0..sep]; const rkey = path[sep + 1 ..]; if (zat.Nsid.parse(collection) == null) return error.InvalidFrame; if (rkey.len > 0) { if (zat.Rkey.parse(rkey) == null) return error.InvalidFrame; } } else return error.InvalidFrame; // path must contain '/' } } }, else => return error.InvalidFrame, } } }
fn queueResolve(self: *Validator, did: []const u8) void { // check if already cached (race between validate and resolver) if (self.cache.contains(did)) return;
const duped = self.allocator.dupe(u8, did) catch return;
self.queue_mutex.lockUncancelable(self.io); defer self.queue_mutex.unlock(self.io);
// skip if already queued (prevents unbounded queue growth) if (self.queued_set.contains(duped)) { self.allocator.free(duped); return; }
// cap queue size — drop DID without adding to queued_set so it can be re-queued later if (self.queue.items.len >= max_queue_size) { self.allocator.free(duped); return; }
self.queue.append(self.allocator, duped) catch { self.allocator.free(duped); return; }; self.queued_set.put(self.allocator, duped, {}) catch {}; self.queue_cond.signal(self.io); }
fn resolveLoop(self: *Validator) void { var resolver = zat.DidResolver.initWithOptions(self.io, self.allocator, .{ .keep_alive = true }); defer resolver.deinit();
while (self.alive.load(.acquire)) { var did: ?[]const u8 = null; { self.queue_mutex.lockUncancelable(self.io); defer self.queue_mutex.unlock(self.io); while (self.queue.items.len == 0 and self.alive.load(.acquire)) { self.queue_cond.waitUncancelable(self.io, &self.queue_mutex); } if (self.queue.items.len > 0) { did = self.queue.orderedRemove(0); _ = self.queued_set.remove(did.?); } }
const d = did orelse continue; defer self.allocator.free(d);
// skip if already cached (resolved while queued) if (self.cache.contains(d)) continue;
// resolve DID → signing key const parsed = zat.Did.parse(d) orelse continue; var doc = resolver.resolve(parsed) catch |err| { log.debug("DID resolve failed for {s}: {s}", .{ d, @errorName(err) }); continue; }; defer doc.deinit();
// extract and decode signing key const vm = doc.signingKey() orelse continue; const key_bytes = zat.multibase.decode(self.allocator, vm.public_key_multibase) catch continue; defer self.allocator.free(key_bytes); const public_key = zat.multicodec.parsePublicKey(key_bytes) catch continue;
// store decoded key in cache (fixed-size, no pointer chasing) var cached = CachedKey{ .key_type = public_key.key_type, .raw = undefined, .len = @intCast(public_key.raw.len), .resolve_time = timestamp(self.io), }; @memcpy(cached.raw[0..public_key.raw.len], public_key.raw);
self.cache.put(d, cached) catch continue;
// --- host validation (merged from migration queue) --- // while we have the DID doc, check PDS endpoint and update host if needed. // best-effort: failures don't prevent signing key caching. if (self.persist) |persist| { if (doc.pdsEndpoint()) |pds_endpoint| { if (extractHostFromUrl(pds_endpoint)) |pds_host| { const pds_host_id = (persist.getHostIdForHostname(pds_host) catch null) orelse continue; const uid = persist.uidForDid(d) catch continue; const current_host = persist.getAccountHostId(uid) catch continue; if (current_host != 0 and current_host != pds_host_id) { persist.setAccountHostId(uid, pds_host_id) catch {}; log.info("host updated via DID doc: {s} -> host {d}", .{ d, pds_host_id }); } } } } } }
/// seed a DID's signing key directly, bypassing DID-document resolution. /// the conformance harness needs the corpus keypair in the cache so the /// commit path takes the cache-hit branch instead of skipping on a miss. pub fn seedSigningKey( self: *Validator, did: []const u8, public_key: zat.multicodec.PublicKey, resolve_time: i64, ) !void { if (public_key.raw.len > 33) return error.KeyTooLong; var cached: CachedKey = .{ .key_type = public_key.key_type, .raw = undefined, .len = @intCast(public_key.raw.len), .resolve_time = resolve_time, }; @memcpy(cached.raw[0..public_key.raw.len], public_key.raw); try self.cache.put(did, cached); }
/// evict a DID's cached signing key (e.g. on #identity event). /// the next commit from this DID will trigger a fresh resolution. pub fn evictKey(self: *Validator, did: []const u8) void { _ = self.cache.remove(did); }
/// cache size (for diagnostics) pub fn cacheSize(self: *Validator) u32 { return self.cache.count(); }
/// resolve queue length (for diagnostics — non-blocking) pub fn resolveQueueLen(self: *Validator) usize { if (!self.queue_mutex.tryLock()) return 0; defer self.queue_mutex.unlock(self.io); return self.queue.items.len; }
/// resolve dedup set size (for diagnostics — non-blocking) pub fn resolveQueuedSetCount(self: *Validator) u32 { if (!self.queue_mutex.tryLock()) return 0; defer self.queue_mutex.unlock(self.io); return self.queued_set.count(); }
/// signing key cache hashmap backing capacity (for memory attribution) pub fn cacheMapCapacity(self: *Validator) u32 { return self.cache.mapCapacity(); }
pub fn hostAuthorityCacheSize(self: *Validator) u32 { return self.host_authority_cache.count(); }
/// resolver dedup set hashmap backing capacity (for memory attribution — non-blocking) pub fn resolveQueuedSetCapacity(self: *Validator) u32 { if (!self.queue_mutex.tryLock()) return 0; defer self.queue_mutex.unlock(self.io); return self.queued_set.capacity(); }
pub const HostAuthority = enum { accept, migrate, reject };
/// synchronous host authority check. called on first-seen DIDs (is_new) /// and host migrations (host_changed). resolves the DID doc to verify the /// PDS endpoint matches the incoming host. retries once on failure to /// handle transient network errors. /// /// uses a pooled resolver to avoid creating a fresh resolver (and fresh /// TLS handshake) per call. blocks briefly if all pool slots are in use. /// /// returns: /// .accept — should not happen (caller should only call on new/mismatch) /// .migrate — DID doc confirms this host, caller should update host_id /// .reject — DID doc does not confirm, caller should drop the event pub fn resolveHostAuthority( self: *Validator, did: []const u8, incoming_host_id: u64, incoming_host: []const u8, ) HostAuthority { const persist = self.persist orelse return .migrate; // no DB — can't check const parsed = zat.Did.parse(did) orelse { _ = self.stats.host_authority_reject_parse_did.fetchAdd(1, .monotonic); return .reject; };
if (self.host_authority_cache.recentlyRejected(did, incoming_host_id)) { _ = self.stats.host_authority_cache_hits.fetchAdd(1, .monotonic); return .reject; }
_ = self.stats.host_authority_network_checks.fetchAdd(1, .monotonic); const network_t0 = microTimestamp(self.io); defer { const elapsed: u64 = @intCast(@max(0, microTimestamp(self.io) - network_t0)); _ = self.stats.host_authority_network_time_us.fetchAdd(elapsed, .monotonic); }
const idx = self.acquireHostResolver(); defer self.releaseHostResolver(idx);
var resolver = &self.host_resolvers[idx];
// first resolve attempt var doc = resolver.resolve(parsed) catch { // retry once on network failure var doc2 = resolver.resolve(parsed) catch { _ = self.stats.host_authority_reject_resolve.fetchAdd(1, .monotonic); return .reject; }; defer doc2.deinit(); return self.checkPdsHost(&doc2, persist, did, incoming_host_id, incoming_host); }; defer doc.deinit(); return self.checkPdsHost(&doc, persist, did, incoming_host_id, incoming_host); }
/// acquire a resolver from the pool. spins until one is available. fn acquireHostResolver(self: *Validator) usize { while (self.alive.load(.acquire)) { for (0..host_resolver_pool_size) |i| { if (self.host_resolver_available[i].cmpxchgStrong(true, false, .acquire, .monotonic) == null) { return i; } } self.io.sleep(Io.Duration.fromMilliseconds(1), .awake) catch {}; } return 0; // shutdown path — caller will exit soon }
fn releaseHostResolver(self: *Validator, idx: usize) void { self.host_resolver_available[idx].store(true, .release); }
fn checkPdsHost( self: *Validator, doc: *zat.DidDocument, persist: *event_log_mod.DiskPersist, did: []const u8, incoming_host_id: u64, incoming_host: []const u8, ) HostAuthority { const pds_endpoint = doc.pdsEndpoint() orelse { _ = self.stats.host_authority_reject_no_endpoint.fetchAdd(1, .monotonic); self.sampleLogReject("no_endpoint", did, "", incoming_host_id, 0); return .reject; }; const pds_host = extractHostFromUrl(pds_endpoint) orelse { _ = self.stats.host_authority_reject_bad_url.fetchAdd(1, .monotonic); self.sampleLogReject("bad_url", did, pds_endpoint, incoming_host_id, 0); return .reject; }; const pds_host_id = (persist.getHostIdForHostname(pds_host) catch null) orelse { _ = self.stats.host_authority_reject_unknown_host.fetchAdd(1, .monotonic); self.sampleLogReject("unknown_host", did, pds_host, incoming_host_id, 0); return .reject; }; if (pds_host_id == incoming_host_id) { self.host_authority_cache.forget(did, incoming_host_id); return .migrate; } const mismatch_index = self.stats.host_authority_reject_host_mismatch.fetchAdd(1, .monotonic) + 1; const first_occurrence = self.host_authority_cache.remember(did, incoming_host_id); if (first_occurrence) { self.sampleLogHostMismatch(mismatch_index, did, incoming_host, pds_host, incoming_host_id, pds_host_id); } return .reject; }
/// Log the first network-confirmed mismatch and then one in every 128. /// Cache hits are intentionally silent: this is visibility into distinct /// authority decisions without recreating the wrong-host log storm. fn sampleLogHostMismatch( _: *Validator, mismatch_index: u64, did: []const u8, incoming_host: []const u8, expected_host: []const u8, incoming_host_id: u64, expected_host_id: u64, ) void { if (mismatch_index != 1 and (mismatch_index & 0x7f) != 0) return; log.warn( "host_authority mismatch did={s} incoming_host={s} incoming_host_id={d} expected_host={s} expected_host_id={d}", .{ did, incoming_host, incoming_host_id, expected_host, expected_host_id }, ); }
/// log a rejection sample at 1-in-2048 rate. at ~10 rejections/sec that's /// one log line every ~3.5min. total rejections per branch are available /// via relay_host_authority_reject{branch=...} in prometheus. fn sampleLogReject( self: *Validator, branch: []const u8, did: []const u8, detail: []const u8, incoming_host_id: u64, resolved_host_id: u64, ) void { const count = self.stats.failed_host_authority.load(.monotonic); // parens mandatory: `&` and `!=` precedence differs from C in zig. if ((count & 0x7ff) != 0) return; log.warn( "host_authority reject branch={s} did={s} detail={s} incoming_host_id={d} resolved_host_id={d}", .{ branch, did, detail, incoming_host_id, resolved_host_id }, ); }};
// --- tests ---
test "host authority cache keys by DID and incoming host and expires at 60 seconds" { var cache = HostAuthorityCache.init(std.testing.allocator, std.testing.io, 8); defer cache.deinit();
try std.testing.expect(cache.rememberAt("did:plc:alice", 7, 100));
try std.testing.expect(cache.recentlyRejectedAt("did:plc:alice", 7, 100)); try std.testing.expect(cache.recentlyRejectedAt("did:plc:alice", 7, 159)); try std.testing.expect(!cache.recentlyRejectedAt("did:plc:alice", 8, 159)); try std.testing.expect(!cache.recentlyRejectedAt("did:plc:bob", 7, 159)); try std.testing.expect(!cache.recentlyRejectedAt("did:plc:alice", 7, 160)); try std.testing.expectEqual(@as(u32, 0), cache.count());}
test "host authority cache clears a mismatch after authority succeeds" { var cache = HostAuthorityCache.init(std.testing.allocator, std.testing.io, 8); defer cache.deinit();
try std.testing.expect(cache.rememberAt("did:plc:alice", 7, 100)); try std.testing.expect(cache.recentlyRejectedAt("did:plc:alice", 7, 120));
cache.forget("did:plc:alice", 7); try std.testing.expect(!cache.recentlyRejectedAt("did:plc:alice", 7, 120));}
test "host authority cache prunes expired entries and remains bounded" { var cache = HostAuthorityCache.init(std.testing.allocator, std.testing.io, 3); defer cache.deinit();
_ = cache.rememberAt("did:plc:expired-a", 1, 0); _ = cache.rememberAt("did:plc:expired-b", 1, 0); _ = cache.rememberAt("did:plc:fresh", 1, 100); _ = cache.rememberAt("did:plc:new", 1, 100);
try std.testing.expectEqual(@as(u32, 2), cache.count()); try std.testing.expect(!cache.recentlyRejectedAt("did:plc:expired-a", 1, 100)); try std.testing.expect(cache.recentlyRejectedAt("did:plc:fresh", 1, 100)); try std.testing.expect(cache.recentlyRejectedAt("did:plc:new", 1, 100));
_ = cache.rememberAt("did:plc:third", 1, 100); _ = cache.rememberAt("did:plc:overflow", 1, 100); try std.testing.expectEqual(@as(u32, 1), cache.count()); try std.testing.expect(!cache.recentlyRejectedAt("did:plc:fresh", 1, 100)); try std.testing.expect(cache.recentlyRejectedAt("did:plc:overflow", 1, 100));}
test "host authority cache handles a 16-worker cached mismatch storm without IO" { const Worker = struct { fn run(cache: *HostAuthorityCache, misses: *std.atomic.Value(u64)) void { for (0..10_000) |_| { if (!cache.recentlyRejectedAt("did:plc:wrong-host", 42, 120)) { _ = misses.fetchAdd(1, .monotonic); } } } };
var cache = HostAuthorityCache.init(std.testing.allocator, std.testing.io, 8192); defer cache.deinit(); _ = cache.rememberAt("did:plc:wrong-host", 42, 100);
var misses: std.atomic.Value(u64) = .{ .raw = 0 }; var threads: [16]std.Thread = undefined; for (&threads) |*thread| { thread.* = try std.Thread.spawn(.{}, Worker.run, .{ &cache, &misses }); } for (&threads) |*thread| thread.join();
try std.testing.expectEqual(@as(u64, 0), misses.load(.acquire)); try std.testing.expectEqual(@as(u32, 1), cache.count());}
test "validateCommit skips on cache miss" { var stats = broadcaster.Stats{}; var v = Validator.init(std.testing.allocator, &stats, std.testing.io); defer v.deinit();
// build a commit payload using SDK const payload: zat.cbor.Value = .{ .map = &.{ .{ .key = "repo", .value = .{ .text = "did:plc:test123" } }, .{ .key = "seq", .value = .{ .unsigned = 42 } }, .{ .key = "rev", .value = .{ .text = "3k2abc000000" } }, .{ .key = "time", .value = .{ .text = "2024-01-15T10:30:00Z" } }, } };
const result = v.validateCommit(payload); try std.testing.expect(result.valid); try std.testing.expect(result.skipped); try std.testing.expectEqual(@as(u64, 1), stats.cache_misses.load(.acquire));}
test "validateCommit skips when no repo field" { var stats = broadcaster.Stats{}; var v = Validator.init(std.testing.allocator, &stats, std.testing.io); defer v.deinit();
// payload without "repo" field const payload: zat.cbor.Value = .{ .map = &.{ .{ .key = "seq", .value = .{ .unsigned = 42 } }, } };
const result = v.validateCommit(payload); try std.testing.expect(result.valid); try std.testing.expect(result.skipped); try std.testing.expectEqual(@as(u64, 1), stats.skipped.load(.acquire));}
test "checkCommitStructure rejects invalid DID" { var stats = broadcaster.Stats{}; var v = Validator.init(std.testing.allocator, &stats, std.testing.io); defer v.deinit();
const payload: zat.cbor.Value = .{ .map = &.{ .{ .key = "repo", .value = .{ .text = "not-a-did" } }, } };
try std.testing.expectError(error.InvalidFrame, v.checkCommitStructure(payload));}
test "checkCommitStructure accepts valid commit" { var stats = broadcaster.Stats{}; var v = Validator.init(std.testing.allocator, &stats, std.testing.io); defer v.deinit();
const payload: zat.cbor.Value = .{ .map = &.{ .{ .key = "repo", .value = .{ .text = "did:plc:test123" } }, .{ .key = "rev", .value = .{ .text = "3k2abcdefghij" } }, } };
try v.checkCommitStructure(payload);}
test "validateSync skips on cache miss" { var stats = broadcaster.Stats{}; var v = Validator.init(std.testing.allocator, &stats, std.testing.io); defer v.deinit();
const payload: zat.cbor.Value = .{ .map = &.{ .{ .key = "did", .value = .{ .text = "did:plc:test123" } }, .{ .key = "seq", .value = .{ .unsigned = 42 } }, .{ .key = "rev", .value = .{ .text = "3k2abcdefghij" } }, .{ .key = "blocks", .value = .{ .bytes = "deadbeef" } }, } };
const result = v.validateSync(payload); try std.testing.expect(result.valid); try std.testing.expect(result.skipped); try std.testing.expectEqual(@as(u64, 1), stats.cache_misses.load(.acquire));}
test "validateSync rejects invalid DID" { var stats = broadcaster.Stats{}; var v = Validator.init(std.testing.allocator, &stats, std.testing.io); defer v.deinit();
const payload: zat.cbor.Value = .{ .map = &.{ .{ .key = "did", .value = .{ .text = "not-a-did" } }, .{ .key = "blocks", .value = .{ .bytes = "deadbeef" } }, } };
const result = v.validateSync(payload); try std.testing.expect(!result.valid); try std.testing.expect(!result.skipped); try std.testing.expectEqual(@as(u64, 1), stats.failed.load(.acquire));}
test "validateSync rejects missing blocks" { var stats = broadcaster.Stats{}; var v = Validator.init(std.testing.allocator, &stats, std.testing.io); defer v.deinit();
const payload: zat.cbor.Value = .{ .map = &.{ .{ .key = "did", .value = .{ .text = "did:plc:test123" } }, .{ .key = "rev", .value = .{ .text = "3k2abcdefghij" } }, } };
const result = v.validateSync(payload); try std.testing.expect(!result.valid); try std.testing.expect(!result.skipped); try std.testing.expectEqual(@as(u64, 1), stats.failed.load(.acquire));}
test "validateSync skips when no did field" { var stats = broadcaster.Stats{}; var v = Validator.init(std.testing.allocator, &stats, std.testing.io); defer v.deinit();
const payload: zat.cbor.Value = .{ .map = &.{ .{ .key = "seq", .value = .{ .unsigned = 42 } }, } };
const result = v.validateSync(payload); try std.testing.expect(result.valid); try std.testing.expect(result.skipped); try std.testing.expectEqual(@as(u64, 1), stats.skipped.load(.acquire));}
test "LRU cache evicts least recently used" { var stats = broadcaster.Stats{}; var v = Validator.init(std.testing.allocator, &stats, std.testing.io); v.cache.capacity = 3; defer v.deinit();
const mk = CachedKey{ .key_type = .p256, .raw = .{0} ** 33, .len = 33 };
try v.cache.put("did:plc:aaa", mk); try v.cache.put("did:plc:bbb", mk); try v.cache.put("did:plc:ccc", mk);
// access "aaa" to promote it _ = v.cache.get("did:plc:aaa");
// insert "ddd" — should evict "bbb" (LRU) try v.cache.put("did:plc:ddd", mk);
try std.testing.expect(v.cache.get("did:plc:bbb") == null); try std.testing.expect(v.cache.get("did:plc:aaa") != null); try std.testing.expect(v.cache.get("did:plc:ccc") != null); try std.testing.expect(v.cache.get("did:plc:ddd") != null); try std.testing.expectEqual(@as(u32, 3), v.cache.count());}
test "checkCommitStructure rejects too many ops" { var stats = broadcaster.Stats{}; var v = Validator.initWithConfig(std.testing.allocator, &stats, .{ .max_ops = 2 }, std.testing.io); defer v.deinit();
// build ops array with 3 items (over limit of 2) const ops = [_]zat.cbor.Value{ .{ .map = &.{.{ .key = "action", .value = .{ .text = "create" } }} }, .{ .map = &.{.{ .key = "action", .value = .{ .text = "create" } }} }, .{ .map = &.{.{ .key = "action", .value = .{ .text = "create" } }} }, };
const payload: zat.cbor.Value = .{ .map = &.{ .{ .key = "repo", .value = .{ .text = "did:plc:test123" } }, .{ .key = "ops", .value = .{ .array = &ops } }, } };
try std.testing.expectError(error.InvalidFrame, v.checkCommitStructure(payload));}
// --- spec conformance tests ---
test "spec: #commit blocks > 2,000,000 bytes rejected" { // lexicon maxLength for #commit blocks: 2,000,000 var stats = broadcaster.Stats{}; var v = Validator.init(std.testing.allocator, &stats, std.testing.io); defer v.deinit();
// insert a fake cached key so we reach the blocks size check const did = "did:plc:test123"; try v.cache.put(did, .{ .key_type = .p256, .raw = .{0} ** 33, .len = 33, .resolve_time = 100, });
// blocks with 2,000,001 bytes (1 byte over limit) const oversized_blocks = try std.testing.allocator.alloc(u8, 2_000_001); defer std.testing.allocator.free(oversized_blocks); @memset(oversized_blocks, 0);
const payload: zat.cbor.Value = .{ .map = &.{ .{ .key = "repo", .value = .{ .text = did } }, .{ .key = "rev", .value = .{ .text = "3k2abcdefghij" } }, .{ .key = "blocks", .value = .{ .bytes = oversized_blocks } }, } };
const result = v.validateCommit(payload); try std.testing.expect(!result.valid or result.skipped);}
test "spec: #commit blocks = 2,000,000 bytes accepted (boundary)" { // lexicon maxLength for #commit blocks: 2,000,000 — exactly at limit should pass size check var stats = broadcaster.Stats{}; var v = Validator.init(std.testing.allocator, &stats, std.testing.io); defer v.deinit();
const did = "did:plc:test123"; try v.cache.put(did, .{ .key_type = .p256, .raw = .{0} ** 33, .len = 33, .resolve_time = 100, });
// exactly 2,000,000 bytes — should pass size check (may fail signature verify, that's ok) const exact_blocks = try std.testing.allocator.alloc(u8, 2_000_000); defer std.testing.allocator.free(exact_blocks); @memset(exact_blocks, 0);
const payload: zat.cbor.Value = .{ .map = &.{ .{ .key = "repo", .value = .{ .text = did } }, .{ .key = "rev", .value = .{ .text = "3k2abcdefghij" } }, .{ .key = "blocks", .value = .{ .bytes = exact_blocks } }, } };
_ = v.validateCommit(payload); // The payload is not a CAR, but it reached verification rather than the // oversized-block rejection branch. try std.testing.expectEqual(@as(u64, 0), stats.failed_oversized_blocks.load(.acquire));}
test "spec: #sync blocks > 10,000 bytes rejected" { // lexicon maxLength for #sync blocks: 10,000 var stats = broadcaster.Stats{}; var v = Validator.init(std.testing.allocator, &stats, std.testing.io); defer v.deinit();
const payload: zat.cbor.Value = .{ .map = &.{ .{ .key = "did", .value = .{ .text = "did:plc:test123" } }, .{ .key = "rev", .value = .{ .text = "3k2abcdefghij" } }, .{ .key = "blocks", .value = .{ .bytes = &([_]u8{0} ** 10_001) } }, } };
const result = v.validateSync(payload); try std.testing.expect(!result.valid); try std.testing.expect(!result.skipped);}
test "spec: #sync blocks = 10,000 bytes accepted (boundary)" { // lexicon maxLength for #sync blocks: 10,000 — exactly at limit should pass size check var stats = broadcaster.Stats{}; var v = Validator.init(std.testing.allocator, &stats, std.testing.io); defer v.deinit();
const payload: zat.cbor.Value = .{ .map = &.{ .{ .key = "did", .value = .{ .text = "did:plc:test123" } }, .{ .key = "rev", .value = .{ .text = "3k2abcdefghij" } }, .{ .key = "blocks", .value = .{ .bytes = &([_]u8{0} ** 10_000) } }, } };
const result = v.validateSync(payload); // should pass size check — will be a cache miss → skipped (no cached key) try std.testing.expect(result.valid); try std.testing.expect(result.skipped);}
test "extractOps reads path field from firehose format" { var stats = broadcaster.Stats{}; var v = Validator.initWithConfig(std.testing.allocator, &stats, .{ .verify_commit_diff = true }, std.testing.io); defer v.deinit();
// use arena since extractOps allocates an ArrayList internally var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit();
const ops = [_]zat.cbor.Value{ .{ .map = &.{ .{ .key = "action", .value = .{ .text = "create" } }, .{ .key = "path", .value = .{ .text = "app.bsky.feed.post/3k2abc000000" } }, .{ .key = "cid", .value = .{ .cid = .{ .raw = "fakecid12345" } } }, } }, .{ .map = &.{ .{ .key = "action", .value = .{ .text = "delete" } }, .{ .key = "path", .value = .{ .text = "app.bsky.feed.like/3k2def000000" } }, .{ .key = "prev", .value = .{ .cid = .{ .raw = "fakecid67890" } } }, } }, };
const payload: zat.cbor.Value = .{ .map = &.{ .{ .key = "repo", .value = .{ .text = "did:plc:test123" } }, .{ .key = "ops", .value = .{ .array = &ops } }, } };
const result = try v.extractOps(arena.allocator(), payload); try std.testing.expectEqual(@as(usize, 2), result.len); try std.testing.expectEqualStrings("app.bsky.feed.post/3k2abc000000", result[0].path); try std.testing.expectEqualStrings("app.bsky.feed.like/3k2def000000", result[1].path); try std.testing.expect(result[0].value != null); // create has cid try std.testing.expect(result[1].value == null); // delete has no cid}
test "extractOps rejects malformed path without slash" { var stats = broadcaster.Stats{}; var v = Validator.initWithConfig(std.testing.allocator, &stats, .{ .verify_commit_diff = true }, std.testing.io); defer v.deinit();
var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit();
const ops = [_]zat.cbor.Value{ .{ .map = &.{ .{ .key = "action", .value = .{ .text = "create" } }, .{ .key = "path", .value = .{ .text = "noslash" } }, .{ .key = "cid", .value = .{ .cid = .{ .raw = "fakecid12345" } } }, } }, };
const payload: zat.cbor.Value = .{ .map = &.{ .{ .key = "repo", .value = .{ .text = "did:plc:test123" } }, .{ .key = "ops", .value = .{ .array = &ops } }, } };
try std.testing.expectError(error.InvalidCommitDiff, v.extractOps(arena.allocator(), payload));}
test "checkCommitStructure validates path field" { var stats = broadcaster.Stats{}; var v = Validator.init(std.testing.allocator, &stats, std.testing.io); defer v.deinit();
// valid path const valid_ops = [_]zat.cbor.Value{ .{ .map = &.{ .{ .key = "action", .value = .{ .text = "create" } }, .{ .key = "path", .value = .{ .text = "app.bsky.feed.post/3k2abc000000" } }, } }, };
const valid_payload: zat.cbor.Value = .{ .map = &.{ .{ .key = "repo", .value = .{ .text = "did:plc:test123" } }, .{ .key = "ops", .value = .{ .array = &valid_ops } }, } };
try v.checkCommitStructure(valid_payload);
// invalid collection in path const invalid_ops = [_]zat.cbor.Value{ .{ .map = &.{ .{ .key = "action", .value = .{ .text = "create" } }, .{ .key = "path", .value = .{ .text = "not-an-nsid/3k2abc000000" } }, } }, };
const invalid_payload: zat.cbor.Value = .{ .map = &.{ .{ .key = "repo", .value = .{ .text = "did:plc:test123" } }, .{ .key = "ops", .value = .{ .array = &invalid_ops } }, } };
try std.testing.expectError(error.InvalidFrame, v.checkCommitStructure(invalid_payload));}
test "queueResolve deduplicates repeated DIDs" { var stats = broadcaster.Stats{}; var v = Validator.init(std.testing.allocator, &stats, std.testing.io); defer v.deinit();
// queue the same DID 100 times for (0..100) |_| { v.queueResolve("did:plc:duplicate"); }
// should have exactly 1 entry, not 100 try std.testing.expectEqual(@as(usize, 1), v.queue.items.len); try std.testing.expectEqual(@as(u32, 1), v.queued_set.count());}
test "prior repo state requires prevData" { var stats = broadcaster.Stats{}; var v = Validator.init(std.testing.allocator, &stats, std.testing.io); defer v.deinit();
const missing: zat.cbor.Value = .{ .map = &.{ .{ .key = "repo", .value = .{ .text = "did:plc:test123" } }, } }; try std.testing.expect(!v.validatePrevDataPresence(missing)); try std.testing.expectEqual(@as(u64, 1), stats.failed.load(.acquire)); try std.testing.expectEqual(@as(u64, 1), stats.failed_prev_data_missing.load(.acquire)); try std.testing.expectEqual(@as(u64, 1), stats.chain_breaks_prev_data.load(.acquire)); // must NOT be attributable to a proof failure: conflating the two is what // let a verifier bug hide behind expected pre-Sync-1.1 sender behavior try std.testing.expectEqual(@as(u64, 0), stats.failed_sync_1_1.load(.acquire)); try std.testing.expectEqual(@as(u64, 0), stats.failed_commit_integrity.load(.acquire));
const cid = try zat.cbor.Cid.forDagCbor(std.testing.allocator, "prior-root"); defer std.testing.allocator.free(cid.raw); const present: zat.cbor.Value = .{ .map = &.{ .{ .key = "repo", .value = .{ .text = "did:plc:test123" } }, .{ .key = "prevData", .value = .{ .cid = cid } }, } }; try std.testing.expect(v.validatePrevDataPresence(present));}
// build a commit CAR with a real signature over the unsigned commit bytes,// so verification reaches the success path (not a sig failure → skip)fn buildSignedCommitCar(a: Allocator, kp: zat.Keypair, did_str: []const u8, rev: []const u8) ![]u8 { const data_cid = try zat.cbor.Cid.forDagCbor(a, "mst-root-placeholder"); const signed = try zat.signCommit(a, .{ .did = did_str, .rev = rev, .data = data_cid, }, &kp);
return zat.car.writeAlloc(a, .{ .roots = &.{signed.cid}, .blocks = &.{.{ .cid_raw = signed.cid.raw, .data = signed.bytes }}, });}
const CommitDiffFixture = struct { car_bytes: []const u8, prev_data: zat.cbor.Cid, data: zat.cbor.Cid, old_record: zat.cbor.Cid, new_record: zat.cbor.Cid, commit: zat.cbor.Cid,};
fn buildCommitDiffFixture(a: Allocator, kp: zat.Keypair, did: []const u8, rev: []const u8) !CommitDiffFixture { const path = "app.bsky.feed.post/3k2abcdefghij"; const old_record_bytes = "old-record"; const new_record_bytes = "new-record"; const old_record = try zat.cbor.Cid.forDagCbor(a, old_record_bytes); const new_record = try zat.cbor.Cid.forDagCbor(a, new_record_bytes);
var before = zat.mst.Mst.init(a); try before.put(path, old_record); const prev_data = try before.rootCid();
var after = try before.copy(); try after.put(path, new_record); const data = try after.rootCid();
const signed = try zat.signCommit(a, .{ .did = did, .rev = rev, .data = data, }, &kp);
var blocks: std.ArrayList(zat.car.Block) = .empty; try blocks.append(a, .{ .cid_raw = signed.cid.raw, .data = signed.bytes }); try after.collectBlocks(&blocks); try blocks.append(a, .{ .cid_raw = new_record.raw, .data = new_record_bytes });
return .{ .car_bytes = try zat.car.writeAlloc(a, .{ .roots = &.{signed.cid}, .blocks = blocks.items, }), .prev_data = prev_data, .data = data, .old_record = old_record, .new_record = new_record, .commit = signed.cid, };}
test "Sync 1.1 accepts commit diff with correct prevData" { var stats = broadcaster.Stats{}; var v = Validator.init(std.testing.allocator, &stats, std.testing.io); defer v.deinit();
var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const a = arena.allocator();
const kp = try zat.Keypair.fromSecretKey(.p256, .{1} ** 32); const did = "did:plc:test123"; const rev = "3k2abcdefghij"; const fixture = try buildCommitDiffFixture(a, kp, did, rev); const pubkey = try kp.publicKey(); try v.cache.put(did, .{ .key_type = .p256, .raw = pubkey, .len = 33, .resolve_time = 100, });
const ops = [_]zat.cbor.Value{.{ .map = &.{ .{ .key = "action", .value = .{ .text = "update" } }, .{ .key = "path", .value = .{ .text = "app.bsky.feed.post/3k2abcdefghij" } }, .{ .key = "cid", .value = .{ .cid = fixture.new_record } }, .{ .key = "prev", .value = .{ .cid = fixture.old_record } }, } }}; const payload: zat.cbor.Value = .{ .map = &.{ .{ .key = "repo", .value = .{ .text = did } }, .{ .key = "rev", .value = .{ .text = rev } }, .{ .key = "commit", .value = .{ .cid = fixture.commit } }, .{ .key = "blocks", .value = .{ .bytes = fixture.car_bytes } }, .{ .key = "ops", .value = .{ .array = &ops } }, .{ .key = "prevData", .value = .{ .cid = fixture.prev_data } }, } };
const result = v.validateCommit(payload); try std.testing.expect(result.valid); try std.testing.expect(!result.skipped); try std.testing.expectEqualSlices(u8, fixture.data.raw, result.data_cid.?); try std.testing.expectEqualStrings(rev, result.commit_rev.?);}
test "Sync 1.1 rejects commit diff with wrong prevData" { var stats = broadcaster.Stats{}; var v = Validator.init(std.testing.allocator, &stats, std.testing.io); defer v.deinit();
var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const a = arena.allocator();
const kp = try zat.Keypair.fromSecretKey(.p256, .{1} ** 32); const did = "did:plc:test123"; const rev = "3k2abcdefghij"; const fixture = try buildCommitDiffFixture(a, kp, did, rev); const wrong_prev_data = try zat.cbor.Cid.forDagCbor(a, "wrong-prev-data"); const pubkey = try kp.publicKey(); try v.cache.put(did, .{ .key_type = .p256, .raw = pubkey, .len = 33, .resolve_time = 100, });
const ops = [_]zat.cbor.Value{.{ .map = &.{ .{ .key = "action", .value = .{ .text = "update" } }, .{ .key = "path", .value = .{ .text = "app.bsky.feed.post/3k2abcdefghij" } }, .{ .key = "cid", .value = .{ .cid = fixture.new_record } }, .{ .key = "prev", .value = .{ .cid = fixture.old_record } }, } }}; const payload: zat.cbor.Value = .{ .map = &.{ .{ .key = "repo", .value = .{ .text = did } }, .{ .key = "rev", .value = .{ .text = rev } }, .{ .key = "commit", .value = .{ .cid = fixture.commit } }, .{ .key = "blocks", .value = .{ .bytes = fixture.car_bytes } }, .{ .key = "ops", .value = .{ .array = &ops } }, .{ .key = "prevData", .value = .{ .cid = wrong_prev_data } }, } };
const result = v.validateCommit(payload); try std.testing.expect(!result.valid); try std.testing.expect(!result.skipped); try std.testing.expectEqual(@as(u64, 1), stats.failed_commit_integrity.load(.acquire)); try std.testing.expectEqual(@as(u64, 1), stats.failed_sync_1_1.load(.acquire)); // the other half of the split: a proof that failed to invert is never // reported as a sender that omitted prevData try std.testing.expectEqual(@as(u64, 0), stats.failed_prev_data_missing.load(.acquire));}
test "wrong signing key rejects commit" { var stats = broadcaster.Stats{}; var v = Validator.initWithConfig(std.testing.allocator, &stats, .{ .verify_commit_diff = false }, std.testing.io); defer v.deinit();
var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const a = arena.allocator();
const signer = try zat.Keypair.fromSecretKey(.p256, .{1} ** 32); const wrong_signer = try zat.Keypair.fromSecretKey(.p256, .{2} ** 32); const did = "did:plc:test123"; const rev = "3k2abcdefghij"; const car_bytes = try buildSignedCommitCar(a, signer, did, rev); const wrong_pubkey = try wrong_signer.publicKey(); try v.cache.put(did, .{ .key_type = .p256, .raw = wrong_pubkey, .len = 33, .resolve_time = 100, });
const payload: zat.cbor.Value = .{ .map = &.{ .{ .key = "repo", .value = .{ .text = did } }, .{ .key = "rev", .value = .{ .text = rev } }, .{ .key = "blocks", .value = .{ .bytes = car_bytes } }, } }; const result = v.validateCommit(payload);
try std.testing.expect(!result.valid); try std.testing.expect(!result.skipped); try std.testing.expectEqual(@as(u64, 1), stats.failed.load(.acquire)); try std.testing.expectEqual(@as(u64, 1), stats.failed_signature.load(.acquire)); try std.testing.expectEqual(@as(u64, 0), stats.skipped.load(.acquire));}
test "wrong signing key rejects sync" { var stats = broadcaster.Stats{}; var v = Validator.init(std.testing.allocator, &stats, std.testing.io); defer v.deinit();
var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const a = arena.allocator();
const signer = try zat.Keypair.fromSecretKey(.p256, .{1} ** 32); const wrong_signer = try zat.Keypair.fromSecretKey(.p256, .{2} ** 32); const did = "did:plc:test123"; const rev = "3k2abcdefghij"; const car_bytes = try buildSignedCommitCar(a, signer, did, rev); const wrong_pubkey = try wrong_signer.publicKey(); try v.cache.put(did, .{ .key_type = .p256, .raw = wrong_pubkey, .len = 33, .resolve_time = 100, });
const payload: zat.cbor.Value = .{ .map = &.{ .{ .key = "did", .value = .{ .text = did } }, .{ .key = "rev", .value = .{ .text = rev } }, .{ .key = "blocks", .value = .{ .bytes = car_bytes } }, } }; const result = v.validateSync(payload);
try std.testing.expect(!result.valid); try std.testing.expect(!result.skipped); try std.testing.expectEqual(@as(u64, 1), stats.failed.load(.acquire)); try std.testing.expectEqual(@as(u64, 1), stats.failed_signature.load(.acquire)); try std.testing.expectEqual(@as(u64, 0), stats.skipped.load(.acquire));}
test "CID mismatch rejects commit" { var stats = broadcaster.Stats{}; var v = Validator.init(std.testing.allocator, &stats, std.testing.io); defer v.deinit();
var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const a = arena.allocator();
const signer = try zat.Keypair.fromSecretKey(.p256, .{1} ** 32); const did = "did:plc:test123"; const rev = "3k2abcdefghij"; const valid_car = try buildSignedCommitCar(a, signer, did, rev); const tampered_car = try a.dupe(u8, valid_car); tampered_car[tampered_car.len - 1] ^= 1; try std.testing.expectError(error.BadBlockHash, zat.car.read(a, tampered_car)); const pubkey = try signer.publicKey(); try v.cache.put(did, .{ .key_type = .p256, .raw = pubkey, .len = 33, .resolve_time = 100, });
const payload: zat.cbor.Value = .{ .map = &.{ .{ .key = "repo", .value = .{ .text = did } }, .{ .key = "rev", .value = .{ .text = rev } }, .{ .key = "blocks", .value = .{ .bytes = tampered_car } }, } }; const result = v.validateCommit(payload);
try std.testing.expect(!result.valid); try std.testing.expect(!result.skipped); try std.testing.expectEqual(@as(u64, 1), stats.failed.load(.acquire)); try std.testing.expectEqual(@as(u64, 1), stats.failed_commit_integrity.load(.acquire)); try std.testing.expectEqual(@as(u64, 0), stats.failed_signature.load(.acquire)); try std.testing.expectEqual(@as(u64, 0), stats.skipped.load(.acquire)); try std.testing.expect(v.cache.contains(did));}
test "CID mismatch rejects sync" { var stats = broadcaster.Stats{}; var v = Validator.init(std.testing.allocator, &stats, std.testing.io); defer v.deinit();
var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const a = arena.allocator();
const signer = try zat.Keypair.fromSecretKey(.p256, .{1} ** 32); const did = "did:plc:test123"; const rev = "3k2abcdefghij"; const valid_car = try buildSignedCommitCar(a, signer, did, rev); const tampered_car = try a.dupe(u8, valid_car); tampered_car[tampered_car.len - 1] ^= 1; const pubkey = try signer.publicKey(); try v.cache.put(did, .{ .key_type = .p256, .raw = pubkey, .len = 33, .resolve_time = 100, });
const payload: zat.cbor.Value = .{ .map = &.{ .{ .key = "did", .value = .{ .text = did } }, .{ .key = "rev", .value = .{ .text = rev } }, .{ .key = "blocks", .value = .{ .bytes = tampered_car } }, } }; const result = v.validateSync(payload);
try std.testing.expect(!result.valid); try std.testing.expect(!result.skipped); try std.testing.expectEqual(@as(u64, 1), stats.failed.load(.acquire)); try std.testing.expectEqual(@as(u64, 1), stats.failed_commit_integrity.load(.acquire)); try std.testing.expectEqual(@as(u64, 0), stats.failed_signature.load(.acquire)); try std.testing.expectEqual(@as(u64, 0), stats.skipped.load(.acquire)); try std.testing.expect(v.cache.contains(did));}
test "regression: commit and sync return the correct data CID" { // Signature-only #commit verification cannot recover the MST root and must // return null. #sync still contains a signed commit object, so extract its // data field rather than mistaking the commit CID for the MST root. var stats = broadcaster.Stats{}; var v = Validator.initWithConfig(std.testing.allocator, &stats, .{ .verify_commit_diff = false }, std.testing.io); defer v.deinit();
var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const a = arena.allocator();
const kp = try zat.Keypair.fromSecretKey(.p256, .{1} ** 32); const did = "did:plc:test123"; const rev = "3k2abcdefghij"; const car_bytes = try buildSignedCommitCar(a, kp, did, rev); const loaded = try zat.loadCommitFromCAR(a, car_bytes);
const pubkey = try kp.publicKey(); try v.cache.put(did, .{ .key_type = .p256, .raw = pubkey, .len = 33, .resolve_time = 100, });
// #commit path (verifyCommit fallback) const commit_payload: zat.cbor.Value = .{ .map = &.{ .{ .key = "repo", .value = .{ .text = did } }, .{ .key = "rev", .value = .{ .text = rev } }, .{ .key = "blocks", .value = .{ .bytes = car_bytes } }, } }; const commit_result = v.validateCommit(commit_payload); try std.testing.expect(commit_result.valid); try std.testing.expect(!commit_result.skipped); try std.testing.expectEqualStrings(rev, commit_result.commit_rev.?); try std.testing.expect(commit_result.data_cid == null);
// #sync path (validateSync) const sync_payload: zat.cbor.Value = .{ .map = &.{ .{ .key = "did", .value = .{ .text = did } }, .{ .key = "rev", .value = .{ .text = rev } }, .{ .key = "blocks", .value = .{ .bytes = car_bytes } }, } }; const sync_result = v.validateSync(sync_payload); try std.testing.expect(sync_result.valid); try std.testing.expect(!sync_result.skipped); try std.testing.expectEqualStrings(rev, sync_result.commit_rev.?); try std.testing.expectEqualSlices(u8, loaded.commit.data_cid, sync_result.data_cid.?);}