lib/machine/src/explore/query/canon.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const explore = @import("../root.zig");
  2 const fabric = @import("../../fabric/root.zig");
  3 const fault = @import("../../fault/root.zig");
  4 const os = @import("os");
  5 const profile = @import("../../profile/root.zig");
  6 const std = @import("std");
  7 const types = @import("types.zig");
  8 const world = @import("../../world/root.zig");
  9 
 10 const Sha256 = std.crypto.hash.sha2.Sha256;
 11 const magic = [8]u8{ 'M', 'C', 'H', 'Q', 'R', 'Y', '1', 0 };
 12 const ref_magic = [8]u8{ 'M', 'C', 'H', 'E', 'V', 'R', '1', 0 };
 13 const domain = "tiny.machine.explore.query.history/v1";
 14 const ref_domain = "tiny.machine.explore.query.reference/v1";
 15 const version: u16 = 1;
 16 const flags: u8 = 0;
 17 const header_bytes: usize = 64;
 18 const moment_bytes: usize = 320;
 19 const decision_bytes: usize = 128;
 20 const event_bytes: usize = 128;
 21 const frame_bytes: usize = decision_bytes + moment_bytes + 8;
 22 const record_bytes: usize = 8 + fabric.transition.stream_bytes;
 23 const digest_bytes: usize = Sha256.digest_length;
 24 
 25 pub const RefWire = [80]u8;
 26 
 27 const Header = struct {
 28     frame_count: u16,
 29     evidence_count: u16,
 30     state: explore.TraceState,
 31 };
 32 
 33 pub fn Wire(comptime capacity: types.Capacity) type {
 34     const bytes = wireBytes(capacity);
 35     if (bytes > std.math.maxInt(u32)) {
 36         @compileError("query history wire size exceeds u32");
 37     }
 38     return [bytes]u8;
 39 }
 40 
 41 pub fn wireBytes(capacity: types.Capacity) usize {
 42     return identityOffset(capacity) + digest_bytes;
 43 }
 44 
 45 pub fn encode(
 46     comptime capacity: types.Capacity,
 47     value: anytype,
 48     output: *Wire(capacity),
 49 ) types.Error!void {
 50     try value.validate();
 51     if (value.state == .open) return error.HistoryOpen;
 52     var encoded: Wire(capacity) = @splat(0);
 53     encodeHeader(capacity, value, &encoded);
 54     encodeMoment(value.start, encoded[startOffset()..framesOffset()]);
 55     for (value.frames(), 0..) |frame, index| {
 56         const offset = frameOffset(capacity, index);
 57         encodeFrame(frame, encoded[offset..][0..frame_bytes]);
 58     }
 59     for (value.evidence(), 0..) |record, index| {
 60         const offset = recordOffset(capacity, index);
 61         try encodeEvidence(record, encoded[offset..][0..record_bytes]);
 62     }
 63     const digest = materialDigest(capacity, &encoded);
 64     @memcpy(encoded[identityOffset(capacity)..], &digest);
 65     output.* = encoded;
 66 }
 67 
 68 pub fn decode(
 69     comptime capacity: types.Capacity,
 70     input: *const Wire(capacity),
 71     output: anytype,
 72 ) types.Error!void {
 73     try validateDigest(capacity, input);
 74     const header = try decodeHeader(capacity, input);
 75     var result: @TypeOf(output.*) = .{
 76         .start = try decodeMoment(input[startOffset()..framesOffset()]),
 77         .frame_count = header.frame_count,
 78         .evidence_count = header.evidence_count,
 79         .state = header.state,
 80         .identity_value = .{
 81             .digest = input[identityOffset(capacity)..][0..digest_bytes].*,
 82         },
 83     };
 84     for (result.frames_storage[0..result.frame_count], 0..) |*frame, index| {
 85         const offset = frameOffset(capacity, index);
 86         frame.* = try decodeFrame(input[offset..][0..frame_bytes]);
 87     }
 88     for (result.evidence_storage[0..result.evidence_count], 0..) |*record, index| {
 89         const offset = recordOffset(capacity, index);
 90         record.* = try decodeEvidence(input[offset..][0..record_bytes]);
 91     }
 92     try result.validate();
 93     var canonical: Wire(capacity) = undefined;
 94     try encode(capacity, &result, &canonical);
 95     if (!std.mem.eql(u8, input, &canonical)) {
 96         return error.CanonicalIdentityMismatch;
 97     }
 98     output.* = result;
 99 }
100 
101 pub fn identity(
102     comptime capacity: types.Capacity,
103     input: *const Wire(capacity),
104 ) types.Error!types.Identity {
105     try validateDigest(capacity, input);
106     return .{ .digest = input[identityOffset(capacity)..][0..digest_bytes].* };
107 }
108 
109 pub fn encodeRef(value: types.EvidenceRef, output: *RefWire) void {
110     var encoded: RefWire = @splat(0);
111     @memcpy(encoded[0..8], &ref_magic);
112     put(u16, &encoded, 8, version);
113     encoded[10] = @backingInt(value.target);
114     encoded[11] = flags;
115     put(u16, &encoded, 12, value.index);
116     @memcpy(encoded[16..48], &value.history.digest);
117     var hasher = Sha256.init(.{});
118     hasher.update(ref_domain);
119     hasher.update(&.{0});
120     hasher.update(encoded[0..48]);
121     var digest: [digest_bytes]u8 = undefined;
122     hasher.final(&digest);
123     @memcpy(encoded[48..80], &digest);
124     output.* = encoded;
125 }
126 
127 pub fn decodeRef(input: *const RefWire) types.Error!types.EvidenceRef {
128     if (!std.mem.eql(u8, input[0..8], &ref_magic)) {
129         return error.EvidenceRefBadMagic;
130     }
131     if (get(u16, input, 8) != version) {
132         return error.EvidenceRefVersionUnsupported;
133     }
134     if (input[11] != flags or !os.abi.wire.allZero(input[14..16])) {
135         return error.EvidenceRefFieldInvalid;
136     }
137     var expected: [digest_bytes]u8 = undefined;
138     var hasher = Sha256.init(.{});
139     hasher.update(ref_domain);
140     hasher.update(&.{0});
141     hasher.update(input[0..48]);
142     hasher.final(&expected);
143     if (!std.mem.eql(u8, &expected, input[48..80])) {
144         return error.EvidenceRefFieldInvalid;
145     }
146     return .{
147         .history = .{ .digest = input[16..48].* },
148         .target = try enumValue(types.Target, input[10]),
149         .index = get(u16, input, 12),
150     };
151 }
152 
153 fn encodeHeader(capacity: types.Capacity, value: anytype, output: []u8) void {
154     @memcpy(output[0..8], &magic);
155     put(u16, output, 8, version);
156     output[10] = flags;
157     put(u32, output, 12, @intCast(output.len));
158     put(u16, output, 16, capacity.frames);
159     put(u16, output, 18, capacity.evidence);
160     put(u16, output, 20, value.frame_count);
161     put(u16, output, 22, value.evidence_count);
162     output[24] = @backingInt(value.state);
163     put(u16, output, 26, moment_bytes);
164     put(u16, output, 28, decision_bytes);
165     put(u16, output, 30, event_bytes);
166     put(u16, output, 32, frame_bytes);
167     put(u16, output, 34, record_bytes);
168     put(u16, output, 36, fabric.transition.stream_bytes);
169     put(u16, output, 38, @sizeOf(os.abi.MessageWire));
170 }
171 
172 fn decodeHeader(
173     comptime capacity: types.Capacity,
174     input: *const Wire(capacity),
175 ) types.Error!Header {
176     if (!std.mem.eql(u8, input[0..8], &magic)) return error.EvidenceWireBadMagic;
177     if (get(u16, input, 8) != version) {
178         return error.EvidenceWireVersionUnsupported;
179     }
180     if (input[10] != flags) return error.EvidenceWireFlagsUnsupported;
181     if (get(u32, input, 12) != input.len) return error.EvidenceWireSizeMismatch;
182     if (get(u16, input, 16) != capacity.frames or
183         get(u16, input, 18) != capacity.evidence)
184     {
185         return error.EvidenceWireCapacityMismatch;
186     }
187     if (get(u16, input, 26) != moment_bytes or
188         get(u16, input, 28) != decision_bytes or
189         get(u16, input, 30) != event_bytes or
190         get(u16, input, 32) != frame_bytes or
191         get(u16, input, 34) != record_bytes or
192         get(u16, input, 36) != fabric.transition.stream_bytes or
193         get(u16, input, 38) != @sizeOf(os.abi.MessageWire) or
194         !os.abi.wire.allZero(input[11..12]) or
195         !os.abi.wire.allZero(input[25..26]) or
196         !os.abi.wire.allZero(input[40..header_bytes]))
197     {
198         return error.EvidenceWireFieldInvalid;
199     }
200     const frame_count = get(u16, input, 20);
201     const evidence_count = get(u16, input, 22);
202     if (frame_count > capacity.frames or evidence_count > capacity.evidence) {
203         return error.EvidenceWireCapacityMismatch;
204     }
205     return .{
206         .frame_count = frame_count,
207         .evidence_count = evidence_count,
208         .state = try enumValue(explore.TraceState, input[24]),
209     };
210 }
211 
212 fn encodeFrame(value: types.Frame, output: []u8) void {
213     encodeDecision(value.decision, output[0..decision_bytes]);
214     encodeMoment(value.expected, output[decision_bytes .. decision_bytes + moment_bytes]);
215     put(u16, output, decision_bytes + moment_bytes, value.evidence_start);
216     put(u16, output, decision_bytes + moment_bytes + 2, value.evidence_count);
217 }
218 
219 fn decodeFrame(input: []const u8) types.Error!types.Frame {
220     return .{
221         .decision = try decodeDecision(input[0..decision_bytes]),
222         .expected = try decodeMoment(
223             input[decision_bytes .. decision_bytes + moment_bytes],
224         ),
225         .evidence_start = get(u16, input, decision_bytes + moment_bytes),
226         .evidence_count = get(u16, input, decision_bytes + moment_bytes + 2),
227     };
228 }
229 
230 fn encodeEvidence(value: types.Evidence, output: []u8) types.Error!void {
231     switch (value) {
232         .transition => |wire| {
233             _ = try fabric.transition.inspect(&wire);
234             output[0] = 1;
235             @memcpy(output[8..][0..wire.len], &wire);
236         },
237         .machine => |wire| {
238             _ = try os.abi.decodeEvent(&wire);
239             output[0] = 2;
240             @memcpy(output[8..][0..wire.len], &wire);
241         },
242         .semantic => |event| {
243             output[0] = 3;
244             encodeEvent(event, output[8..][0..event_bytes]);
245         },
246     }
247 }
248 
249 fn decodeEvidence(input: []const u8) types.Error!types.Evidence {
250     return switch (input[0]) {
251         1 => transition: {
252             const wire: fabric.transition.Wire = input[8..][0..fabric.transition.stream_bytes].*;
253             _ = try fabric.transition.inspect(&wire);
254             break :transition .{ .transition = wire };
255         },
256         2 => machine: {
257             const wire: os.abi.MessageWire = input[8..][0..@sizeOf(os.abi.MessageWire)].*;
258             _ = try os.abi.decodeEvent(&wire);
259             break :machine .{ .machine = wire };
260         },
261         3 => .{ .semantic = try decodeEvent(input[8..][0..event_bytes]) },
262         else => error.EvidenceWireFieldInvalid,
263     };
264 }
265 
266 fn encodeMoment(value: world.Moment, output: []u8) void {
267     @memcpy(output[0..32], &value.digest);
268     put(u16, output, 32, @backingInt(value.dialect));
269     encodeWorldRoot(value.origin, output[40..208]);
270     encodeFabricRoot(value.fabric, output[208..304]);
271 }
272 
273 fn decodeMoment(input: []const u8) types.Error!world.Moment {
274     return .{
275         .digest = input[0..32].*,
276         .dialect = try enumValue(world.MomentDialect, get(u16, input, 32)),
277         .origin = try decodeWorldRoot(input[40..208]),
278         .fabric = try decodeFabricRoot(input[208..304]),
279     };
280 }
281 
282 fn encodeWorldRoot(value: world.Root, output: []u8) void {
283     @memcpy(output[0..32], &value.digest);
284     put(u16, output, 32, @backingInt(value.dialect));
285     output[34] = value.node_count;
286     @memcpy(output[40..72], &value.machine_contract.digest);
287     encodeFabricRoot(value.fabric, output[72..168]);
288 }
289 
290 fn decodeWorldRoot(input: []const u8) types.Error!world.Root {
291     return .{
292         .digest = input[0..32].*,
293         .dialect = try enumValue(world.Dialect, get(u16, input, 32)),
294         .node_count = input[34],
295         .machine_contract = .{ .digest = input[40..72].* },
296         .fabric = try decodeFabricRoot(input[72..168]),
297     };
298 }
299 
300 fn encodeFabricRoot(value: fabric.Root, output: []u8) void {
301     @memcpy(output[0..32], &value.digest);
302     put(u16, output, 32, @backingInt(value.dialect));
303     @memcpy(output[40..72], &value.machine_contract.digest);
304     put(u64, output, 72, value.entry_frontier);
305     put(u64, output, 80, value.admission_frontier);
306     put(u64, output, 88, value.fault_frontier);
307 }
308 
309 fn decodeFabricRoot(input: []const u8) types.Error!fabric.Root {
310     return .{
311         .digest = input[0..32].*,
312         .dialect = try enumValue(fabric.Dialect, get(u16, input, 32)),
313         .machine_contract = .{ .digest = input[40..72].* },
314         .entry_frontier = get(u64, input, 72),
315         .admission_frontier = get(u64, input, 80),
316         .fault_frontier = get(u64, input, 88),
317     };
318 }
319 
320 fn encodeDecision(value: explore.SearchDecision, output: []u8) void {
321     output[0] = @backingInt(value.site.stream);
322     put(u16, output, 2, value.site.sequence);
323     put(u64, output, 4, value.site.virtual_time_tick);
324     output[12] = value.alternative;
325     output[13] = value.alternative_count;
326     switch (value.choice) {
327         .input => |choice| encodeInput(choice, output[16..]),
328         .schedule => |choice| encodeSchedule(choice, output[16..]),
329         .topology => |choice| encodeTopology(choice, output[16..]),
330         .fault => |choice| encodeGeneratedFault(choice, output[16..]),
331     }
332 }
333 
334 fn decodeDecision(input: []const u8) types.Error!explore.SearchDecision {
335     const stream = try enumValue(explore.Stream, input[0]);
336     const choice: explore.SearchChoice = switch (stream) {
337         .input => .{ .input = try decodeInput(input[16..]) },
338         .schedule => .{ .schedule = try decodeSchedule(input[16..]) },
339         .topology => .{ .topology = try decodeTopology(input[16..]) },
340         .fault => .{ .fault = try decodeGeneratedFault(input[16..]) },
341     };
342     return .{
343         .site = .{
344             .stream = stream,
345             .sequence = get(u16, input, 2),
346             .virtual_time_tick = get(u64, input, 4),
347         },
348         .alternative = input[12],
349         .alternative_count = input[13],
350         .choice = choice,
351     };
352 }
353 
354 fn encodeOrigin(value: explore.Origin, output: []u8) void {
355     put(u16, output, 0, @backingInt(value.source));
356     put(u16, output, 2, value.version);
357 }
358 
359 fn decodeOrigin(input: []const u8) types.Error!explore.Origin {
360     return .{
361         .source = try enumValue(profile.DeterminismSource, get(u16, input, 0)),
362         .version = get(u16, input, 2),
363     };
364 }
365 
366 fn encodeInput(value: explore.GeneratedInput, output: []u8) void {
367     encodeOrigin(value.origin, output[0..4]);
368     output[4] = @backingInt(std.meta.activeTag(value.value));
369     put(u64, output, 8, value.virtual_time_tick);
370     switch (value.value) {
371         .wait => |payload| put(u64, output, 16, payload),
372         .terminal => |payload| put(u64, output, 16, payload),
373         .entropy => |payload| put(u64, output, 16, payload),
374         .packet => |payload| put(u64, output, 16, payload),
375         .service_result => |payload| output[16] = @backingInt(payload),
376         .effect_result => |payload| output[16] = @backingInt(payload),
377     }
378 }
379 
380 fn decodeInput(input: []const u8) types.Error!explore.GeneratedInput {
381     const kind = try enumValue(explore.InputKind, input[4]);
382     const value: explore.InputValue = switch (kind) {
383         .wait => .{ .wait = get(u64, input, 16) },
384         .terminal => .{ .terminal = get(u64, input, 16) },
385         .entropy => .{ .entropy = get(u64, input, 16) },
386         .packet => .{ .packet = get(u64, input, 16) },
387         .service_result => .{ .service_result = try enumValue(
388             explore.OperationOutcome,
389             input[16],
390         ) },
391         .effect_result => .{ .effect_result = try enumValue(
392             explore.OperationOutcome,
393             input[16],
394         ) },
395     };
396     return .{
397         .origin = try decodeOrigin(input[0..4]),
398         .virtual_time_tick = get(u64, input, 8),
399         .value = value,
400     };
401 }
402 
403 fn encodeSchedule(value: explore.GeneratedSchedule, output: []u8) void {
404     encodeOrigin(value.origin, output[0..4]);
405     output[4] = @backingInt(std.meta.activeTag(value.value));
406     put(u64, output, 8, value.virtual_time_tick);
407     switch (value.value) {
408         .turn => |node| output[16] = node,
409         .idle => {},
410     }
411 }
412 
413 fn decodeSchedule(input: []const u8) types.Error!explore.GeneratedSchedule {
414     const kind = try enumValue(explore.ScheduleKind, input[4]);
415     const value: explore.ScheduleValue = switch (kind) {
416         .turn => .{ .turn = input[16] },
417         .idle => .idle,
418     };
419     return .{
420         .origin = try decodeOrigin(input[0..4]),
421         .virtual_time_tick = get(u64, input, 8),
422         .value = value,
423     };
424 }
425 
426 fn encodeTopology(value: explore.GeneratedTopology, output: []u8) void {
427     encodeOrigin(value.origin, output[0..4]);
428     output[4] = @backingInt(std.meta.activeTag(value.value));
429     put(u64, output, 8, value.virtual_time_tick);
430     switch (value.value) {
431         .node => |node| {
432             @memcpy(output[16..32], &node.id.bytes);
433             output[32] = @intFromBool(node.included);
434         },
435         .complete => {},
436     }
437 }
438 
439 fn decodeTopology(input: []const u8) types.Error!explore.GeneratedTopology {
440     const kind = try enumValue(explore.TopologyKind, input[4]);
441     const value: explore.TopologyValue = switch (kind) {
442         .node => .{ .node = .{
443             .id = .{ .bytes = input[16..32].* },
444             .included = try boolValue(input[32]),
445         } },
446         .complete => .complete,
447     };
448     return .{
449         .origin = try decodeOrigin(input[0..4]),
450         .virtual_time_tick = get(u64, input, 8),
451         .value = value,
452     };
453 }
454 
455 fn encodeGeneratedFault(value: explore.GeneratedFault, output: []u8) void {
456     encodeOrigin(value.choice_origin, output[0..4]);
457     if (value.effect_origin) |origin| {
458         output[4] = 1;
459         encodeOrigin(origin, output[8..12]);
460     }
461     put(u64, output, 16, value.virtual_time_tick);
462     switch (value.action) {
463         .healthy => output[5] = 1,
464         .inject => |kind| {
465             output[5] = 2;
466             output[6] = @backingInt(kind);
467         },
468         .persist => |kind| {
469             output[5] = 3;
470             output[6] = @backingInt(kind);
471         },
472         .recover => |kind| {
473             output[5] = 4;
474             output[6] = @backingInt(kind);
475         },
476     }
477 }
478 
479 fn decodeGeneratedFault(input: []const u8) types.Error!explore.GeneratedFault {
480     const effect_origin: ?explore.Origin = switch (input[4]) {
481         0 => null,
482         1 => try decodeOrigin(input[8..12]),
483         else => return error.EvidenceWireFieldInvalid,
484     };
485     const action: explore.FaultAction = switch (input[5]) {
486         1 => .healthy,
487         2 => .{ .inject = try enumValue(fault.Kind, input[6]) },
488         3 => .{ .persist = try enumValue(fault.Kind, input[6]) },
489         4 => .{ .recover = try enumValue(fault.Kind, input[6]) },
490         else => return error.EvidenceWireFieldInvalid,
491     };
492     return .{
493         .choice_origin = try decodeOrigin(input[0..4]),
494         .effect_origin = effect_origin,
495         .virtual_time_tick = get(u64, input, 16),
496         .action = action,
497     };
498 }
499 
500 fn encodeEvent(value: explore.Event, output: []u8) void {
501     put(u16, output, 0, value.sequence);
502     put(u64, output, 8, value.virtual_time_tick);
503     switch (value.value) {
504         .controlled_input => |event| {
505             output[16] = 1;
506             encodeInput(event, output[24..]);
507         },
508         .schedule_choice => |event| {
509             output[16] = 2;
510             encodeSchedule(event, output[24..]);
511         },
512         .topology_choice => |event| {
513             output[16] = 3;
514             encodeTopology(event, output[24..]);
515         },
516         .observation => |event| {
517             output[16] = 4;
518             put(u64, output, 24, event.id);
519             put(u64, output, 32, event.value);
520         },
521         .property_declaration => |event| {
522             output[16] = 5;
523             encodeDeclaration(event, output[24..]);
524         },
525         .property_evaluation => |event| {
526             output[16] = 6;
527             encodeEvaluation(event, output[24..]);
528         },
529         .injected_fault => |event| {
530             output[16] = 7;
531             encodeGeneratedFault(event, output[24..]);
532         },
533         .diagnostic => |event| {
534             output[16] = 8;
535             put(u64, output, 24, event.id);
536             put(u32, output, 32, event.code);
537         },
538         .external_admission => |event| {
539             output[16] = 9;
540             put(u64, output, 24, event.id);
541             encodeOrigin(event.origin, output[32..36]);
542         },
543         .operation => |event| {
544             output[16] = 10;
545             put(u64, output, 24, event.id);
546             output[32] = @backingInt(event.outcome);
547         },
548     }
549 }
550 
551 fn decodeEvent(input: []const u8) types.Error!explore.Event {
552     const value: explore.EventValue = switch (input[16]) {
553         1 => .{ .controlled_input = try decodeInput(input[24..]) },
554         2 => .{ .schedule_choice = try decodeSchedule(input[24..]) },
555         3 => .{ .topology_choice = try decodeTopology(input[24..]) },
556         4 => .{ .observation = .{
557             .id = get(u64, input, 24),
558             .value = get(u64, input, 32),
559         } },
560         5 => .{ .property_declaration = try decodeDeclaration(input[24..]) },
561         6 => .{ .property_evaluation = try decodeEvaluation(input[24..]) },
562         7 => .{ .injected_fault = try decodeGeneratedFault(input[24..]) },
563         8 => .{ .diagnostic = .{
564             .id = get(u64, input, 24),
565             .code = get(u32, input, 32),
566         } },
567         9 => .{ .external_admission = .{
568             .id = get(u64, input, 24),
569             .origin = try decodeOrigin(input[32..36]),
570         } },
571         10 => .{ .operation = .{
572             .id = get(u64, input, 24),
573             .outcome = try enumValue(explore.OperationOutcome, input[32]),
574         } },
575         else => return error.EvidenceWireFieldInvalid,
576     };
577     return .{
578         .sequence = get(u16, input, 0),
579         .virtual_time_tick = get(u64, input, 8),
580         .value = value,
581     };
582 }
583 
584 fn encodeDeclaration(value: explore.PropertyDeclaration, output: []u8) void {
585     put(u64, output, 0, value.id);
586     output[8] = @backingInt(value.kind);
587     if (value.bound) |bound| {
588         output[9] = 1;
589         switch (bound) {
590             .steps => |steps| {
591                 output[10] = 1;
592                 put(u16, output, 16, steps);
593             },
594             .virtual_time => |ticks| {
595                 output[10] = 2;
596                 put(u64, output, 16, ticks);
597             },
598         }
599     }
600 }
601 
602 fn decodeDeclaration(input: []const u8) types.Error!explore.PropertyDeclaration {
603     const bound: ?explore.Bound = switch (input[9]) {
604         0 => null,
605         1 => switch (input[10]) {
606             1 => .{ .steps = get(u16, input, 16) },
607             2 => .{ .virtual_time = get(u64, input, 16) },
608             else => return error.EvidenceWireFieldInvalid,
609         },
610         else => return error.EvidenceWireFieldInvalid,
611     };
612     return .{
613         .id = get(u64, input, 0),
614         .kind = try enumValue(explore.PropertyKind, input[8]),
615         .bound = bound,
616     };
617 }
618 
619 fn encodeEvaluation(value: explore.PropertyEvaluation, output: []u8) void {
620     put(u64, output, 0, value.id);
621     output[8] = @backingInt(value.verdict);
622     output[9] = @backingInt(value.reason);
623     if (value.witness) |witness| {
624         output[10] = 1;
625         put(u16, output, 12, witness);
626     }
627 }
628 
629 fn decodeEvaluation(input: []const u8) types.Error!explore.PropertyEvaluation {
630     const witness: ?u16 = switch (input[10]) {
631         0 => null,
632         1 => get(u16, input, 12),
633         else => return error.EvidenceWireFieldInvalid,
634     };
635     return .{
636         .id = get(u64, input, 0),
637         .verdict = try enumValue(explore.Verdict, input[8]),
638         .reason = try enumValue(explore.EvaluationReason, input[9]),
639         .witness = witness,
640     };
641 }
642 
643 fn validateDigest(
644     comptime capacity: types.Capacity,
645     input: *const Wire(capacity),
646 ) types.Error!void {
647     const expected = materialDigest(capacity, input);
648     if (!std.mem.eql(u8, &expected, input[identityOffset(capacity)..])) {
649         return error.CanonicalIdentityMismatch;
650     }
651 }
652 
653 fn materialDigest(
654     comptime capacity: types.Capacity,
655     input: *const Wire(capacity),
656 ) [digest_bytes]u8 {
657     var hasher = Sha256.init(.{});
658     hasher.update(domain);
659     hasher.update(&.{0});
660     hasher.update(input[0..identityOffset(capacity)]);
661     var result: [digest_bytes]u8 = undefined;
662     hasher.final(&result);
663     return result;
664 }
665 
666 fn startOffset() usize {
667     return header_bytes;
668 }
669 
670 fn framesOffset() usize {
671     return startOffset() + moment_bytes;
672 }
673 
674 fn frameOffset(capacity: types.Capacity, index: usize) usize {
675     std.debug.assert(index < capacity.frames);
676     return framesOffset() + index * frame_bytes;
677 }
678 
679 fn recordsOffset(capacity: types.Capacity) usize {
680     return framesOffset() + @as(usize, capacity.frames) * frame_bytes;
681 }
682 
683 fn recordOffset(capacity: types.Capacity, index: usize) usize {
684     std.debug.assert(index < capacity.evidence);
685     return recordsOffset(capacity) + index * record_bytes;
686 }
687 
688 fn identityOffset(capacity: types.Capacity) usize {
689     return recordsOffset(capacity) + @as(usize, capacity.evidence) * record_bytes;
690 }
691 
692 fn enumValue(comptime T: type, value: anytype) types.Error!T {
693     inline for (std.meta.tags(T)) |candidate| {
694         if (@backingInt(candidate) == value) return candidate;
695     }
696     return error.EvidenceWireFieldInvalid;
697 }
698 
699 fn boolValue(value: u8) types.Error!bool {
700     return switch (value) {
701         0 => false,
702         1 => true,
703         else => error.EvidenceWireFieldInvalid,
704     };
705 }
706 
707 fn put(comptime T: type, output: []u8, offset: usize, value: T) void {
708     const end = offset + @sizeOf(T);
709     std.debug.assert(end <= output.len);
710     std.mem.writeInt(T, output[offset..][0..@sizeOf(T)], value, .little);
711 }
712 
713 fn get(comptime T: type, input: []const u8, offset: usize) T {
714     const end = offset + @sizeOf(T);
715     std.debug.assert(end <= input.len);
716     return std.mem.readInt(T, input[offset..][0..@sizeOf(T)], .little);
717 }