lib/machine/src/instance/receipt/owner.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const admission = @import("../../admission/root.zig");
  2 const os = @import("os");
  3 const std = @import("std");
  4 const types = @import("types.zig");
  5 
  6 const Sha256 = std.crypto.hash.sha2.Sha256;
  7 const receipt_domain = "TINYMACHINEQUIESCENCE2\x00";
  8 const semantic_domain = "TINYMACHINEQUIESCENCESTATE1\x00";
  9 const transcript_domain = "TINYMACHINEEVENTS1\x00";
 10 const semantic_transcript_domain = "TINYMACHINESEMANTICEVENTS1\x00";
 11 const canonical_fence: os.abi.ActivationFence = .{
 12     .world = @splat(0x51),
 13     .generation = 1,
 14     .token = @splat(0x52),
 15 };
 16 
 17 /// Every check in this file fails in the same single way, so this error reports
 18 /// the one failure a receipt check encounters. The error means the value
 19 /// disagrees with itself or with one of the digests bound into it.
 20 pub const Error = error{InvalidQuiescenceReceipt};
 21 
 22 const EventBatchOwnerError = error{
 23     EventBatchCountInvalid,
 24     EventBatchFenceMismatch,
 25     EventTranscriptMismatch,
 26 };
 27 
 28 /// Checking a batch can go wrong in more ways than checking a receipt, so this
 29 /// error set reports the failures coming from decoding the bytes, matching the
 30 /// fence, checking the receipt, bounding the count, or comparing the
 31 /// transcript.
 32 pub const EventBatchError = os.abi.message.Error ||
 33     os.abi.wire.FenceError ||
 34     Error ||
 35     EventBatchOwnerError;
 36 
 37 /// Issuing a receipt takes the whole boundary at once, so this structure
 38 /// gathers everything needed to issue the authenticated evidence for one
 39 /// acknowledged quiescent turn, or quiescence receipt. Each field is copied
 40 /// into the value the call returns, once the checks pass.
 41 pub const Material = struct {
 42     fence: os.abi.ActivationFence,
 43     delivery: admission.DeliveryReceipt,
 44     admission_receipt: admission.Receipt,
 45     basis: admission.Basis,
 46     image_digest: os.abi.Digest,
 47     execution_fingerprint: types.ExecutionFingerprint,
 48     block_root: os.abi.BlockRoot,
 49     boundary: os.abi.Quiescence,
 50     settled: types.SettledTransport,
 51     k0: types.K0State,
 52     event_transcript_digest: os.abi.Digest,
 53     semantic_transcript_digest: os.abi.Digest,
 54 };
 55 
 56 /// Checks a settled boundary, then hashes it into the digest inside a
 57 /// quiescence receipt that commits every one of its fields, activation
 58 /// authority included, as a fenced digest, so a caller holds the evidence a
 59 /// turn leaves behind to check again later. Material that disagrees with itself
 60 /// comes back as `InvalidQuiescenceReceipt`, and no digest is produced.
 61 pub fn issue(material: Material) Error!types.QuiescenceReceipt {
 62     var value: types.QuiescenceReceipt = .{
 63         .fenced_digest = undefined,
 64         .fence = material.fence,
 65         .delivery = material.delivery,
 66         .admission_receipt = material.admission_receipt,
 67         .basis = material.basis,
 68         .image_digest = material.image_digest,
 69         .execution_fingerprint = material.execution_fingerprint,
 70         .block_root = material.block_root,
 71         .boundary = material.boundary,
 72         .settled = material.settled,
 73         .k0 = material.k0,
 74         .event_transcript_digest = material.event_transcript_digest,
 75         .semantic_transcript_digest = material.semantic_transcript_digest,
 76     };
 77     try validateMaterial(value);
 78     value.fenced_digest = digest(value);
 79     return value;
 80 }
 81 
 82 /// Whoever holds the bytes can check them without the machine that made them,
 83 /// so the call runs every invariant again and recomputes the fenced digest to
 84 /// compare.
 85 pub fn verify(value: types.QuiescenceReceipt) Error!void {
 86     try validateMaterial(value);
 87     const expected = digest(value);
 88     if (!std.mem.eql(u8, &expected, &value.fenced_digest)) {
 89         return error.InvalidQuiescenceReceipt;
 90     }
 91 }
 92 
 93 /// A valid receipt from some other turn is worse than none, so the call checks
 94 /// the receipt and then requires its world, generation, and token to match the
 95 /// ones handed in.
 96 pub fn verifyForFence(
 97     value: types.QuiescenceReceipt,
 98     expected_fence: os.abi.ActivationFence,
 99 ) Error!void {
100     try verify(value);
101     if (!os.abi.wire.equalFence(value.fence, expected_fence)) {
102         return error.InvalidQuiescenceReceipt;
103     }
104 }
105 
106 /// The events a turn produced have to belong to that turn, so the call decodes
107 /// each filled slot and requires the handed-in fence on every encoded message
108 /// in the kernel wire form, or frame. A fence that fails its own check, a count
109 /// past the slots, or a frame that will not decode all come back as
110 /// `EventBatchError`.
111 pub fn verifyEventBatch(
112     batch: *const types.EventBatch,
113     expected_fence: os.abi.ActivationFence,
114 ) EventBatchError!void {
115     try os.abi.wire.validateFence(expected_fence);
116     if (batch.count > batch.storage.len) {
117         return error.EventBatchCountInvalid;
118     }
119     for (batch.storage[0..batch.count]) |*frame| {
120         const decoded = try os.abi.decodeEvent(frame);
121         if (!os.abi.wire.equalFence(decoded.header.fence, expected_fence)) {
122             return error.EventBatchFenceMismatch;
123         }
124     }
125 }
126 
127 /// One call ties a batch, a receipt, and a fence together, so the call checks
128 /// the receipt against the fence, compares the batch's raw transcript digest
129 /// with the one the receipt carries, and then checks every frame.
130 pub fn verifyEventBatchReceipt(
131     batch: *const types.EventBatch,
132     value: types.QuiescenceReceipt,
133     expected_fence: os.abi.ActivationFence,
134 ) EventBatchError!void {
135     try verifyForFence(value, expected_fence);
136     if (batch.count == 0 or batch.count > batch.storage.len) {
137         return error.EventBatchCountInvalid;
138     }
139     if (!std.mem.eql(
140         u8,
141         &transcriptDigest(batch),
142         &value.event_transcript_digest,
143     )) {
144         return error.EventTranscriptMismatch;
145     }
146     try verifyEventBatch(batch, expected_fence);
147 }
148 
149 /// Two receipts whose retained evidence is equal reach the same digest whatever
150 /// activation authority each carries, so the call checks the receipt first,
151 /// then hashes the form of it that leaves out activation authority. Matching
152 /// guest memory alone does not give equal digests.
153 pub fn semanticDigest(value: types.QuiescenceReceipt) Error!os.abi.Digest {
154     try verify(value);
155     return semanticReceiptHash(semanticReceipt(value));
156 }
157 
158 /// A checkpoint keeps the authority-free form, so the call checks the receipt
159 /// and copies out the form checkpoints work from.
160 pub fn projectSemantic(
161     value: types.QuiescenceReceipt,
162 ) Error!types.SemanticReceipt {
163     try verify(value);
164     return semanticReceipt(value);
165 }
166 
167 /// A semantic receipt read back out of storage has to stand on its own, so the
168 /// call runs every invariant the authority-free form has. A value that
169 /// disagrees with itself comes back as `InvalidQuiescenceReceipt`.
170 pub fn verifySemantic(value: types.SemanticReceipt) Error!void {
171     try validateSemantic(value);
172 }
173 
174 /// Naming a stored guest state takes its digest, so the call checks the value,
175 /// then hashes its canonical fields.
176 pub fn semanticReceiptDigest(
177     value: types.SemanticReceipt,
178 ) Error!os.abi.Digest {
179     try validateSemantic(value);
180     return semanticReceiptHash(value);
181 }
182 
183 fn semanticReceiptHash(value: types.SemanticReceipt) os.abi.Digest {
184     var hasher = Sha256.init(.{});
185     hasher.update(semantic_domain);
186     hasher.update(&value.admission_receipt.digest);
187     hasher.update(&value.basis.contract.digest);
188     hasher.update(&value.basis.source_root);
189     hashFrontiers(&hasher, value.basis.frontiers);
190     hashInteger(&hasher, @as(u8, 0));
191     hasher.update(&value.image_digest);
192     hasher.update(&value.execution_fingerprint.digest);
193     hashInteger(&hasher, value.block_root.generation);
194     hasher.update(&value.block_root.digest);
195     hashInteger(&hasher, value.boundary.request_sequence);
196     hashInteger(&hasher, value.boundary.semantic_frontier);
197     hashInteger(&hasher, value.boundary.effect_frontier);
198     hashInteger(&hasher, value.boundary.terminal_offset);
199     hashInteger(&hasher, value.boundary.virtual_time_tick);
200     hashInteger(&hasher, value.boundary.entropy_generation);
201     hashInteger(&hasher, value.boundary.request_consumed);
202     hashInteger(&hasher, value.boundary.request_produced);
203     hashInteger(&hasher, value.boundary.event_consumed);
204     hashInteger(&hasher, value.boundary.event_produced);
205     hashInteger(&hasher, value.boundary.unresolved_effects);
206     hashInteger(&hasher, @backingInt(value.boundary.scheduler));
207     hasher.update(&value.boundary.block_root);
208     hashInteger(&hasher, value.boundary.input_frontier);
209     hashInteger(&hasher, value.boundary.terminal_input_offset);
210     hashInteger(&hasher, value.settled.request_cursor);
211     hashInteger(&hasher, value.settled.event_cursor);
212     hashInteger(&hasher, value.k0.counter);
213     hasher.update(&value.semantic_transcript_digest);
214     var output: os.abi.Digest = undefined;
215     hasher.final(&output);
216     return output;
217 }
218 
219 fn semanticReceipt(value: types.QuiescenceReceipt) types.SemanticReceipt {
220     return .{
221         .admission_receipt = value.admission_receipt,
222         .basis = value.basis,
223         .image_digest = value.image_digest,
224         .execution_fingerprint = value.execution_fingerprint,
225         .block_root = value.block_root,
226         .boundary = .{
227             .request_sequence = value.boundary.request_sequence,
228             .semantic_frontier = value.boundary.semantic_frontier,
229             .effect_frontier = value.boundary.effect_frontier,
230             .terminal_offset = value.boundary.terminal_offset,
231             .virtual_time_tick = value.boundary.virtual_time_tick,
232             .entropy_generation = value.boundary.entropy_generation,
233             .request_consumed = value.boundary.request_consumed,
234             .request_produced = value.boundary.request_produced,
235             .event_consumed = value.boundary.event_consumed,
236             .event_produced = value.boundary.event_produced,
237             .unresolved_effects = value.boundary.unresolved_effects,
238             .scheduler = value.boundary.scheduler,
239             .block_root = value.boundary.block_root,
240             .input_frontier = value.boundary.input_frontier,
241             .terminal_input_offset = value.boundary.terminal_input_offset,
242         },
243         .settled = value.settled,
244         .k0 = value.k0,
245         .semantic_transcript_digest = value.semantic_transcript_digest,
246     };
247 }
248 
249 /// A receipt binds itself to the events a turn produced, so the call hashes the
250 /// filled slots as raw bytes, together with how many there are and the order
251 /// they sit in. The count has to be at least one and no more than
252 /// `event_batch_max`, which the call asserts.
253 pub fn transcriptDigest(batch: *const types.EventBatch) os.abi.Digest {
254     std.debug.assert(batch.count > 0);
255     std.debug.assert(batch.count <= batch.storage.len);
256     var hasher = Sha256.init(.{});
257     hasher.update(transcript_domain);
258     hashInteger(&hasher, batch.count);
259     for (batch.frames()) |*frame| hasher.update(frame);
260     var output: os.abi.Digest = undefined;
261     hasher.final(&output);
262     return output;
263 }
264 
265 /// The same events under a later fence have to reach the same value, so the
266 /// call decodes each event, puts the fixed world, generation, and token of the
267 /// canonical fence in place of each event header's authority, and hashes the
268 /// result. The call also replaces a quiescent event's `capability_generation`
269 /// field with that same fixed generation, and keeps each event's sequence
270 /// number, its correlation, and the rest of its payload as they were. The count
271 /// has to be at least one and no more than `event_batch_max`, which the call
272 /// asserts. Bytes that will not decode come back as a message error.
273 pub fn semanticTranscriptDigest(
274     batch: *const types.EventBatch,
275 ) os.abi.message.Error!os.abi.Digest {
276     std.debug.assert(batch.count > 0);
277     std.debug.assert(batch.count <= batch.storage.len);
278     var hasher = Sha256.init(.{});
279     hasher.update(semantic_transcript_domain);
280     hashInteger(&hasher, batch.count);
281     for (batch.frames()) |*frame| {
282         const decoded = try os.abi.decodeEvent(frame);
283         var value = decoded.value;
284         if (std.meta.activeTag(value) == .quiescent) {
285             value.quiescent.capability_generation = canonical_fence.generation;
286         }
287         var canonical: os.abi.MessageWire = undefined;
288         try os.abi.encodeEvent(
289             canonical_fence,
290             decoded.header.sequence,
291             decoded.header.correlation,
292             value,
293             &canonical,
294         );
295         hasher.update(&canonical);
296     }
297     var output: os.abi.Digest = undefined;
298     hasher.final(&output);
299     return output;
300 }
301 
302 fn validateMaterial(value: types.QuiescenceReceipt) Error!void {
303     os.abi.wire.validateFence(value.fence) catch
304         return error.InvalidQuiescenceReceipt;
305     validateDigest(value.delivery.digest) catch
306         return error.InvalidQuiescenceReceipt;
307     validateDigest(value.event_transcript_digest) catch
308         return error.InvalidQuiescenceReceipt;
309     try validateSemantic(semanticReceipt(value));
310     if (value.boundary.capability_generation != value.fence.generation) {
311         return error.InvalidQuiescenceReceipt;
312     }
313 }
314 
315 fn validateSemantic(value: types.SemanticReceipt) Error!void {
316     validateDigest(value.admission_receipt.digest) catch
317         return error.InvalidQuiescenceReceipt;
318     admission.validateBasis(value.basis) catch
319         return error.InvalidQuiescenceReceipt;
320     if (value.basis.outstanding_effect != null) {
321         return error.InvalidQuiescenceReceipt;
322     }
323     validateDigest(value.image_digest) catch
324         return error.InvalidQuiescenceReceipt;
325     validateDigest(value.execution_fingerprint.digest) catch
326         return error.InvalidQuiescenceReceipt;
327     validateDigest(value.block_root.digest) catch
328         return error.InvalidQuiescenceReceipt;
329     validateDigest(value.semantic_transcript_digest) catch
330         return error.InvalidQuiescenceReceipt;
331     const boundary: os.abi.Quiescence = .{
332         .request_sequence = value.boundary.request_sequence,
333         .semantic_frontier = value.boundary.semantic_frontier,
334         .effect_frontier = value.boundary.effect_frontier,
335         .terminal_offset = value.boundary.terminal_offset,
336         .virtual_time_tick = value.boundary.virtual_time_tick,
337         .entropy_generation = value.boundary.entropy_generation,
338         .request_consumed = value.boundary.request_consumed,
339         .request_produced = value.boundary.request_produced,
340         .event_consumed = value.boundary.event_consumed,
341         .event_produced = value.boundary.event_produced,
342         .capability_generation = canonical_fence.generation,
343         .unresolved_effects = value.boundary.unresolved_effects,
344         .scheduler = value.boundary.scheduler,
345         .block_root = value.boundary.block_root,
346         .input_frontier = value.boundary.input_frontier,
347         .terminal_input_offset = value.boundary.terminal_input_offset,
348     };
349     var frame: os.abi.MessageWire = undefined;
350     os.abi.encodeQuiescentEvent(
351         canonical_fence,
352         boundary.event_produced,
353         boundary,
354         &frame,
355     ) catch return error.InvalidQuiescenceReceipt;
356     if (value.block_root.generation == 0 or
357         value.block_root.generation > std.math.maxInt(u32) or
358         value.boundary.semantic_frontier >= std.math.maxInt(u32) or
359         value.block_root.generation != value.boundary.semantic_frontier + 1 or
360         !std.mem.eql(
361             u8,
362             &value.boundary.block_root,
363             &value.block_root.digest,
364         ) or
365         value.basis.frontiers.input != value.boundary.input_frontier or
366         value.basis.frontiers.terminal_input_offset !=
367             value.boundary.terminal_input_offset or
368         value.basis.frontiers.virtual_time_tick !=
369             value.boundary.virtual_time_tick or
370         value.basis.frontiers.entropy_generation !=
371             value.boundary.entropy_generation or
372         value.basis.frontiers.effect != value.boundary.effect_frontier or
373         value.settled.request_cursor != value.boundary.request_consumed or
374         value.settled.request_cursor != value.boundary.request_produced or
375         value.settled.event_cursor != value.boundary.event_produced or
376         value.k0.counter > value.boundary.semantic_frontier)
377     {
378         return error.InvalidQuiescenceReceipt;
379     }
380 }
381 
382 fn digest(value: types.QuiescenceReceipt) os.abi.Digest {
383     var frame: os.abi.MessageWire = undefined;
384     os.abi.encodeQuiescentEvent(
385         value.fence,
386         value.boundary.event_produced,
387         value.boundary,
388         &frame,
389     ) catch unreachable;
390     var hasher = Sha256.init(.{});
391     hasher.update(receipt_domain);
392     hasher.update(&frame);
393     hasher.update(&value.delivery.digest);
394     hasher.update(&value.admission_receipt.digest);
395     hasher.update(&value.basis.contract.digest);
396     hasher.update(&value.basis.source_root);
397     hashFrontiers(&hasher, value.basis.frontiers);
398     hashInteger(&hasher, @as(u8, 0));
399     hasher.update(&value.image_digest);
400     hasher.update(&value.execution_fingerprint.digest);
401     hashInteger(&hasher, value.block_root.generation);
402     hasher.update(&value.block_root.digest);
403     hashInteger(&hasher, value.settled.request_cursor);
404     hashInteger(&hasher, value.settled.event_cursor);
405     hashInteger(&hasher, value.k0.counter);
406     hasher.update(&value.event_transcript_digest);
407     hasher.update(&value.semantic_transcript_digest);
408     var output: os.abi.Digest = undefined;
409     hasher.final(&output);
410     return output;
411 }
412 
413 fn hashFrontiers(hasher: *Sha256, value: admission.Frontiers) void {
414     hashInteger(hasher, value.input);
415     hashInteger(hasher, value.terminal_input_offset);
416     hashInteger(hasher, value.virtual_time_tick);
417     hashInteger(hasher, value.entropy_generation);
418     hashInteger(hasher, value.effect);
419 }
420 
421 fn hashInteger(hasher: *Sha256, value: anytype) void {
422     var encoded: [@sizeOf(@TypeOf(value))]u8 = undefined;
423     std.mem.writeInt(@TypeOf(value), &encoded, value, .little);
424     hasher.update(&encoded);
425 }
426 
427 fn validateDigest(value: os.abi.Digest) os.abi.wire.DigestError!void {
428     try os.abi.wire.validateDigest(value);
429 }
430 
431 test "quiescence receipt has one canonical digest" {
432     const value = try issue(exampleMaterial());
433     try verify(value);
434     const actual = std.fmt.bytesToHex(value.fenced_digest, .lower);
435     try std.testing.expectEqualStrings(
436         "77d309ec469f7bafb27041504f5f29df5276c81c377e376d144a925a5e0c0e71",
437         &actual,
438     );
439 }
440 
441 test "semantic receipt commits canonical evidence and excludes activation authority" {
442     const first_material = exampleMaterial();
443     const first = try issue(first_material);
444     const expected = try semanticDigest(first);
445 
446     var changed = first_material;
447     changed.admission_receipt.digest[0] ^= 1;
448     try expectDifferentSemantic(expected, try issue(changed));
449     changed = first_material;
450     changed.semantic_transcript_digest[0] ^= 1;
451     try expectDifferentSemantic(expected, try issue(changed));
452     changed = first_material;
453     changed.boundary.request_sequence += 1;
454     changed.boundary.request_consumed += 1;
455     changed.boundary.request_produced += 1;
456     changed.settled.request_cursor += 1;
457     try expectDifferentSemantic(expected, try issue(changed));
458     changed = first_material;
459     changed.boundary.event_consumed += 1;
460     try expectDifferentSemantic(expected, try issue(changed));
461 
462     changed = first_material;
463     changed.fence.world[0] ^= 1;
464     changed.fence.generation += 1;
465     changed.fence.token[0] ^= 1;
466     changed.delivery.digest[0] ^= 1;
467     changed.boundary.capability_generation = changed.fence.generation;
468     const reframed = try issue(changed);
469     try std.testing.expectEqualSlices(
470         u8,
471         &expected,
472         &(try semanticDigest(reframed)),
473     );
474 }
475 
476 test "quiescence receipt rejects changed authority and basis bindings" {
477     const value = try issue(exampleMaterial());
478     var changed = value;
479     changed.fenced_digest[0] ^= 1;
480     try expectRejected(changed);
481     changed = value;
482     changed.fence.world[0] ^= 1;
483     try expectRejected(changed);
484     changed = value;
485     changed.fence.generation += 1;
486     try expectRejected(changed);
487     changed = value;
488     changed.fence.token[0] ^= 1;
489     try expectRejected(changed);
490     changed = value;
491     changed.delivery.digest[0] ^= 1;
492     try expectRejected(changed);
493     changed = value;
494     changed.admission_receipt.digest[0] ^= 1;
495     try expectRejected(changed);
496     changed = value;
497     changed.basis.contract.digest[0] ^= 1;
498     try expectRejected(changed);
499     changed = value;
500     changed.basis.source_root[0] ^= 1;
501     try expectRejected(changed);
502     changed = value;
503     changed.basis.frontiers.input += 1;
504     try expectRejected(changed);
505     changed = value;
506     changed.basis.frontiers.terminal_input_offset += 1;
507     try expectRejected(changed);
508     changed = value;
509     changed.basis.frontiers.virtual_time_tick += 1;
510     try expectRejected(changed);
511     changed = value;
512     changed.basis.frontiers.entropy_generation += 1;
513     try expectRejected(changed);
514     changed = value;
515     changed.basis.frontiers.effect += 1;
516     try expectRejected(changed);
517     changed = value;
518     changed.basis.outstanding_effect = .{
519         .receipt = .{ .digest = @splat(0x88) },
520         .correlation = changed.basis.frontiers.effect + 1,
521     };
522     try expectRejected(changed);
523     changed = value;
524     changed.image_digest[0] ^= 1;
525     try expectRejected(changed);
526     changed = value;
527     changed.execution_fingerprint.digest[0] ^= 1;
528     try expectRejected(changed);
529     changed = value;
530     changed.block_root.generation += 1;
531     try expectRejected(changed);
532     changed = value;
533     changed.block_root.digest[0] ^= 1;
534     try expectRejected(changed);
535 }
536 
537 test "quiescence receipt rejects changed boundary and settlement bindings" {
538     const value = try issue(exampleMaterial());
539     var changed = value;
540     changed.boundary.request_sequence += 1;
541     try expectRejected(changed);
542     changed = value;
543     changed.boundary.semantic_frontier += 1;
544     try expectRejected(changed);
545     changed = value;
546     changed.boundary.effect_frontier += 1;
547     try expectRejected(changed);
548     changed = value;
549     changed.boundary.terminal_offset += 1;
550     try expectRejected(changed);
551     changed = value;
552     changed.boundary.virtual_time_tick += 1;
553     try expectRejected(changed);
554     changed = value;
555     changed.boundary.entropy_generation += 1;
556     try expectRejected(changed);
557     changed = value;
558     changed.boundary.request_consumed += 1;
559     try expectRejected(changed);
560     changed = value;
561     changed.boundary.request_produced += 1;
562     try expectRejected(changed);
563     changed = value;
564     changed.boundary.event_consumed += 1;
565     try expectRejected(changed);
566     changed = value;
567     changed.boundary.event_produced += 1;
568     try expectRejected(changed);
569     changed = value;
570     changed.boundary.capability_generation += 1;
571     try expectRejected(changed);
572     changed = value;
573     changed.boundary.unresolved_effects += 1;
574     try expectRejected(changed);
575     changed = value;
576     changed.boundary.block_root[0] ^= 1;
577     try expectRejected(changed);
578     changed = value;
579     changed.boundary.input_frontier += 1;
580     try expectRejected(changed);
581     changed = value;
582     changed.boundary.terminal_input_offset += 1;
583     try expectRejected(changed);
584     changed = value;
585     changed.settled.request_cursor += 1;
586     try expectRejected(changed);
587     changed = value;
588     changed.settled.event_cursor += 1;
589     try expectRejected(changed);
590     changed = value;
591     changed.k0.counter += 1;
592     try expectRejected(changed);
593     changed = value;
594     changed.event_transcript_digest[0] ^= 1;
595     try expectRejected(changed);
596     changed = value;
597     changed.semantic_transcript_digest[0] ^= 1;
598     try expectRejected(changed);
599 }
600 
601 test "quiescence receipt creation rejects inconsistent stopped cuts" {
602     const value = exampleMaterial();
603     var changed = value;
604     changed.boundary.capability_generation += 1;
605     try expectMaterialRejected(changed);
606     changed = value;
607     changed.basis.frontiers.input += 1;
608     try expectMaterialRejected(changed);
609     changed = value;
610     changed.block_root.digest[0] ^= 1;
611     try expectMaterialRejected(changed);
612     changed = value;
613     changed.settled.request_cursor += 1;
614     try expectMaterialRejected(changed);
615     changed = value;
616     changed.basis.outstanding_effect = .{
617         .receipt = .{ .digest = @splat(0x88) },
618         .correlation = changed.basis.frontiers.effect + 1,
619     };
620     try expectMaterialRejected(changed);
621     changed = value;
622     changed.block_root.generation = 0;
623     try expectMaterialRejected(changed);
624     changed = value;
625     changed.block_root.generation = @as(u64, std.math.maxInt(u32)) + 1;
626     try expectMaterialRejected(changed);
627     changed = value;
628     changed.block_root.generation += 1;
629     try expectMaterialRejected(changed);
630     changed = value;
631     changed.boundary.semantic_frontier = std.math.maxInt(u32);
632     try expectMaterialRejected(changed);
633     changed = value;
634     changed.k0.counter = @intCast(changed.boundary.semantic_frontier + 1);
635     try expectMaterialRejected(changed);
636     changed = value;
637     changed.boundary.unresolved_effects = 1;
638     try expectMaterialRejected(changed);
639 }
640 
641 test "event transcript binds count order and every byte" {
642     var batch: types.EventBatch = .{
643         .count = 2,
644         .storage = @splat(@splat(0)),
645     };
646     batch.storage[0] = @splat(0x11);
647     batch.storage[1] = @splat(0x22);
648     const expected = transcriptDigest(&batch);
649 
650     batch.storage[1][255] ^= 1;
651     try std.testing.expect(!std.mem.eql(u8, &expected, &transcriptDigest(&batch)));
652     batch.storage[1][255] ^= 1;
653     const first = batch.storage[0];
654     batch.storage[0] = batch.storage[1];
655     batch.storage[1] = first;
656     try std.testing.expect(!std.mem.eql(u8, &expected, &transcriptDigest(&batch)));
657     batch.storage[0] = first;
658     batch.storage[1] = @splat(0x22);
659     batch.count = 1;
660     try std.testing.expect(!std.mem.eql(u8, &expected, &transcriptDigest(&batch)));
661 }
662 
663 test "semantic event transcript removes only activation authority" {
664     const first_fence: os.abi.ActivationFence = .{
665         .world = @splat(0x61),
666         .generation = 7,
667         .token = @splat(0x62),
668     };
669     const second_fence: os.abi.ActivationFence = .{
670         .world = @splat(0x71),
671         .generation = 11,
672         .token = @splat(0x72),
673     };
674     const semantic: os.abi.Semantic = .{
675         .interface_id = @splat(0x31),
676         .event_id = 4,
677         .position = 9,
678         .bytes = "state",
679     };
680     var first: types.EventBatch = .{
681         .count = 1,
682         .storage = @splat(@splat(0)),
683     };
684     var second = first;
685     try os.abi.encodeSemanticEvent(first_fence, 3, semantic, &first.storage[0]);
686     try os.abi.encodeSemanticEvent(second_fence, 3, semantic, &second.storage[0]);
687     try std.testing.expect(!std.mem.eql(
688         u8,
689         &transcriptDigest(&first),
690         &transcriptDigest(&second),
691     ));
692     try std.testing.expectEqualSlices(
693         u8,
694         &(try semanticTranscriptDigest(&first)),
695         &(try semanticTranscriptDigest(&second)),
696     );
697 
698     try os.abi.encodeSemanticEvent(
699         second_fence,
700         3,
701         .{
702             .interface_id = semantic.interface_id,
703             .event_id = semantic.event_id + 1,
704             .position = semantic.position,
705             .bytes = semantic.bytes,
706         },
707         &second.storage[0],
708     );
709     try std.testing.expect(!std.mem.eql(
710         u8,
711         &(try semanticTranscriptDigest(&first)),
712         &(try semanticTranscriptDigest(&second)),
713     ));
714 }
715 
716 fn exampleMaterial() Material {
717     const root: os.abi.Digest = @splat(0x55);
718     return .{
719         .fence = .{
720             .world = @splat(0x11),
721             .generation = 7,
722             .token = @splat(0x22),
723         },
724         .delivery = .{ .digest = @splat(0x33) },
725         .admission_receipt = .{ .digest = @splat(0x34) },
726         .basis = .{
727             .contract = .{ .digest = @splat(0x44) },
728             .source_root = @splat(0x45),
729             .frontiers = .{
730                 .input = 3,
731                 .terminal_input_offset = 9,
732                 .virtual_time_tick = 21,
733                 .entropy_generation = 2,
734                 .effect = 1,
735             },
736             .outstanding_effect = null,
737         },
738         .image_digest = @splat(0x46),
739         .execution_fingerprint = .{ .digest = @splat(0x47) },
740         .block_root = .{ .generation = 3, .digest = root },
741         .boundary = .{
742             .request_sequence = 3,
743             .semantic_frontier = 2,
744             .effect_frontier = 1,
745             .terminal_offset = 19,
746             .virtual_time_tick = 21,
747             .entropy_generation = 2,
748             .request_consumed = 3,
749             .request_produced = 3,
750             .event_consumed = 2,
751             .event_produced = 6,
752             .capability_generation = 7,
753             .unresolved_effects = 0,
754             .scheduler = .idle,
755             .block_root = root,
756             .input_frontier = 3,
757             .terminal_input_offset = 9,
758         },
759         .settled = .{ .request_cursor = 3, .event_cursor = 6 },
760         .k0 = .{ .counter = 1 },
761         .event_transcript_digest = @splat(0x48),
762         .semantic_transcript_digest = @splat(0x49),
763     };
764 }
765 
766 fn expectRejected(value: types.QuiescenceReceipt) !void {
767     try std.testing.expectError(error.InvalidQuiescenceReceipt, verify(value));
768 }
769 
770 fn expectMaterialRejected(value: Material) !void {
771     try std.testing.expectError(error.InvalidQuiescenceReceipt, issue(value));
772 }
773 
774 fn expectDifferentSemantic(
775     expected: os.abi.Digest,
776     value: types.QuiescenceReceipt,
777 ) !void {
778     try std.testing.expect(!std.mem.eql(
779         u8,
780         &expected,
781         &(try semanticDigest(value)),
782     ));
783 }