lib/closure/src/ledger/store.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const capacity = @import("capacity");
2 const closure = @import("../root.zig");
3 const std = @import("std");
4
5 const limits = closure.limits;
6 const schema = closure.schema;
7
8 const Slot = enum(u8) {
9 first,
10 second,
11 };
12
13 const StoredPhase = enum(u8) {
14 initialization,
15 steady,
16 teardown,
17 };
18
19 const Header = extern struct {
20 magic: u64,
21 self_address: usize,
22 slot_state: u8,
23 phase: StoredPhase,
24 reserved: [6]u8,
25 };
26
27 const Counts = extern struct {
28 nodes: u16,
29 edges: u16,
30 artifacts: u16,
31 ranges: u16,
32 digests: u16,
33 provenance_parents: u16,
34 source_records: u16,
35 build_records: u16,
36 authorities: u16,
37 lineage_references: u16,
38 service_descriptors: u16,
39 residual_roots: u16,
40 claims: u16,
41 claim_nodes: u16,
42 };
43
44 const SlotHeader = extern struct {
45 generation: schema.GenerationId,
46 root: schema.Digest,
47 counts: Counts,
48 reserved: [4]u8,
49 };
50
51 const ProjectionScratchModel = struct {
52 visited: []u8,
53 queue: []u32,
54 };
55
56 const OwnerError = error{
57 CopiedOwner,
58 InvalidPhase,
59 };
60
61 const StagingError = error{GenerationAlreadyStaged};
62 const PublicationError = error{NoStagedGeneration};
63
64 const ValidationError = error{
65 GenerationOutOfOrder,
66 UnknownGenerationRoot,
67 GenerationRootMismatch,
68 NonCanonicalNode,
69 NonCanonicalEdge,
70 NonCanonicalArtifact,
71 NonCanonicalRange,
72 NonCanonicalDigest,
73 NonCanonicalProvenanceParent,
74 NonCanonicalSourceRecord,
75 NonCanonicalBuildRecord,
76 NonCanonicalAuthority,
77 NonCanonicalLineageReference,
78 NonCanonicalServiceDescriptor,
79 NonCanonicalResidualRoot,
80 NonCanonicalClaim,
81 NonCanonicalClaimNode,
82 UnknownNodeReference,
83 UnknownArtifactReference,
84 UnknownClaimReference,
85 InvalidByteRange,
86 UnknownProfile,
87 };
88
89 const LedgerExhaustion = error{
90 NodeFull,
91 EdgeFull,
92 ArtifactFull,
93 RangeFull,
94 DigestFull,
95 ProvenanceParentFull,
96 SourceRecordFull,
97 BuildRecordFull,
98 AuthorityFull,
99 LineageReferenceFull,
100 ServiceDescriptorFull,
101 ResidualRootFull,
102 ClaimFull,
103 ClaimNodeFull,
104 GenerationExhausted,
105 };
106
107 const header_magic: u64 = 0x434c_4f53_5552_4531;
108 const slot_none: u8 = 2;
109 const slot_mask: u8 = 0x3;
110
111 pub const Ledger = struct {
112 phase: capacity.Phase,
113 capacity: Capacity,
114 storage: Storage,
115
116 pub const Limits: type = closure.Limits;
117 pub const Capacity: type = closure.Capacity;
118 pub const storage_alignment: usize = limits.storage_alignment;
119 pub const Storage = []align(storage_alignment) u8;
120 pub const Exhaustion: type = LedgerExhaustion;
121 pub const InitError: type =
122 Capacity.DeriveError || error{StorageTooShort};
123 pub const PrepareError: type =
124 OwnerError || ValidationError || Exhaustion || StagingError;
125 pub const PublishError: type = OwnerError || PublicationError;
126 pub const RecoveryError: type = OwnerError;
127 pub const SnapshotError: type = OwnerError;
128 pub const ScratchError: type = OwnerError;
129 pub const ProjectionScratch: type = ProjectionScratchModel;
130 pub const work_limits: capacity.WorkLimits = .{
131 .transition_steps_max = 4_000_000,
132 .cleanup_steps_per_call_max = 1,
133 .cleanup_calls_at_capacity_max = 1,
134 };
135 pub const claim: capacity.Declaration = .{
136 .source = .{
137 .id = "closure.ledger",
138 .kind = .startup_static,
139 .limit_source = .caller,
140 .storage = .{
141 .covered = &.{
142 .{
143 .id = "caller_provisioned_double_buffered_closure_ledger_storage",
144 .lifetime = .steady,
145 .detail = "caller-provisioned double-buffered closure ledger storage",
146 },
147 },
148 .excluded = &.{
149 "ledger owner handle",
150 "borrowed GenerationInput source slices",
151 "external artifact bytes",
152 },
153 },
154 .capacity = .{
155 .inputs = &.{
156 capacity.bindInput(Limits, "artifacts", "artifacts"),
157 capacity.bindInput(Limits, "authorities", "authorities"),
158 capacity.bindInput(Limits, "build_records", "build_records"),
159 capacity.bindInput(Limits, "claim_nodes", "claim_nodes"),
160 capacity.bindInput(Limits, "claims", "claims"),
161 capacity.bindInput(Limits, "digests", "digests"),
162 capacity.bindInput(Limits, "edges", "edges"),
163 capacity.bindInput(Limits, "lineage_references", "lineage_references"),
164 capacity.bindInput(Limits, "nodes", "nodes"),
165 capacity.bindInput(Limits, "provenance_parents", "provenance_parents"),
166 capacity.bindInput(Limits, "ranges", "ranges"),
167 capacity.bindInput(Limits, "receipt_bytes", "receipt_bytes"),
168 capacity.bindInput(Limits, "residual_roots", "residual_roots"),
169 capacity.bindInput(Limits, "service_descriptors", "service_descriptors"),
170 capacity.bindInput(Limits, "source_records", "source_records"),
171 },
172 .type_selectors = &.{},
173 .nodes = &.{
174 .{ .input = 0 },
175 .{ .input = 1 },
176 .{ .input = 2 },
177 .{ .input = 3 },
178 .{ .input = 4 },
179 .{ .input = 5 },
180 .{ .input = 6 },
181 .{ .input = 7 },
182 .{ .input = 8 },
183 .{ .input = 9 },
184 .{ .input = 10 },
185 .{ .input = 11 },
186 .{ .input = 12 },
187 .{ .input = 13 },
188 .{ .input = 14 },
189 .{ .add = .{ .left = 0, .right = 1 } },
190 .{ .add = .{ .left = 15, .right = 2 } },
191 .{ .add = .{ .left = 16, .right = 3 } },
192 .{ .add = .{ .left = 17, .right = 4 } },
193 .{ .add = .{ .left = 18, .right = 5 } },
194 .{ .add = .{ .left = 19, .right = 6 } },
195 .{ .add = .{ .left = 20, .right = 7 } },
196 .{ .add = .{ .left = 21, .right = 8 } },
197 .{ .add = .{ .left = 22, .right = 9 } },
198 .{ .add = .{ .left = 23, .right = 10 } },
199 .{ .add = .{ .left = 24, .right = 11 } },
200 .{ .add = .{ .left = 25, .right = 12 } },
201 .{ .add = .{ .left = 26, .right = 13 } },
202 .{ .add = .{ .left = 27, .right = 14 } },
203 },
204 .assertions = &.{.{
205 .scope = .closure_total,
206 .measure = .retained,
207 .relation = .upper_bound,
208 .expression = 28,
209 }},
210 },
211 .overload = .{
212 .kind = .reject_before_mutation,
213 .detail = "every record occupancy and generation overflow rejects before copy",
214 },
215 .risks = .{
216 .transitive = .{
217 .status = .excluded,
218 .detail = "ledger storage and validation retain no allocator capability",
219 },
220 .foreign = .{
221 .status = .excluded,
222 .detail = "ledger assembly reads only typed caller-owned records",
223 },
224 },
225 .work = .{ .equation = "prepare validation and copy <= 4000000 bounded steps" },
226 .obligations = &.{
227 .{ .key = "closure_ledger_capacity_capacity_model", .role = .capacity_model },
228 .{ .key = "closure_ledger_capacity_transitive_risk", .role = .transitive_risk },
229 .{ .key = "closure_ledger_overload", .role = .overload },
230 .{ .key = "closure_ledger_validation", .role = .foreign_risk },
231 .{ .key = "closure_ledger_recovery", .role = .custom },
232 .{ .key = "closure_ledger_work", .role = .work_bound },
233 },
234 },
235 .bindings = .{
236 .owner = @This(),
237 },
238 };
239
240 pub fn init(
241 storage: Storage,
242 requested: Limits,
243 ) InitError!Ledger {
244 const derived = try Capacity.derive(requested);
245 if (storage.len < derived.storage_bytes) {
246 return error.StorageTooShort;
247 }
248 @memset(storage[0..derived.storage_bytes], 0);
249 const header = pointerAt(Header, storage, derived.layout.header);
250 header.* = .{
251 .magic = header_magic,
252 .self_address = 0,
253 .slot_state = encodeSlots(null, null),
254 .phase = .initialization,
255 .reserved = @splat(0),
256 };
257 return .{
258 .phase = .initialization,
259 .capacity = derived,
260 .storage = storage,
261 };
262 }
263
264 pub fn activate(self: *Ledger) void {
265 const header = self.sharedHeader();
266 std.debug.assert(self.phase == .initialization);
267 std.debug.assert(header.phase == .initialization);
268 std.debug.assert(header.self_address == 0);
269 header.self_address = @intFromPtr(self);
270 header.phase = .steady;
271 self.phase = .steady;
272 }
273
274 pub fn prepare(
275 self: *Ledger,
276 input: schema.GenerationInput,
277 ) PrepareError!void {
278 try self.requireOwned();
279 const state = self.loadSlots();
280 if (stagedSlot(state) != null) {
281 return error.GenerationAlreadyStaged;
282 }
283 try validateOccupancy(self.capacity, input);
284 try self.validateGeneration(activeSlot(state), input.id);
285 try validateCanonical(input);
286 const target = inactiveSlot(activeSlot(state));
287 copyInput(self.storage, self.slotLayout(target), input);
288 self.storeSlots(encodeSlots(activeSlot(state), target));
289 }
290
291 pub fn publish(self: *Ledger) PublishError!void {
292 try self.requireOwned();
293 const state = self.loadSlots();
294 const staged = stagedSlot(state) orelse {
295 return error.NoStagedGeneration;
296 };
297 std.debug.assert(activeSlot(state) != staged);
298 self.storeSlots(encodeSlots(staged, null));
299 }
300
301 pub fn recover(self: *Ledger) RecoveryError!bool {
302 try self.requireOwned();
303 const state = self.loadSlots();
304 if (stagedSlot(state) == null) return false;
305 self.storeSlots(encodeSlots(activeSlot(state), null));
306 return true;
307 }
308
309 pub fn snapshot(
310 self: *const Ledger,
311 ) SnapshotError!?schema.GenerationInput {
312 try self.requireOwned();
313 const slot = activeSlot(self.loadSlots()) orelse return null;
314 return inputView(
315 self.storage,
316 self.slotLayout(slot),
317 );
318 }
319
320 pub fn projectionScratch(
321 self: *Ledger,
322 ) ScratchError!ProjectionScratch {
323 try self.requireOwned();
324 const visited_end = self.capacity.layout.visited +
325 self.capacity.nodes;
326 return .{
327 .visited = self.storage[self.capacity.layout.visited..visited_end],
328 .queue = sliceAt(
329 u32,
330 self.storage,
331 self.capacity.layout.queue,
332 self.capacity.nodes,
333 ),
334 };
335 }
336
337 pub fn receiptScratch(self: *Ledger) ScratchError![]u8 {
338 try self.requireOwned();
339 const end = self.capacity.layout.receipt +
340 self.capacity.receipt_bytes;
341 return self.storage[self.capacity.layout.receipt..end];
342 }
343
344 pub fn deinit(self: *Ledger) Storage {
345 self.requireOwned() catch unreachable;
346 const state = self.loadSlots();
347 if (stagedSlot(state) != null) {
348 self.storeSlots(encodeSlots(activeSlot(state), null));
349 }
350 self.sharedHeader().phase = .teardown;
351 self.phase = .teardown;
352 const storage = self.storage;
353 self.* = undefined;
354 return storage;
355 }
356
357 fn validateGeneration(
358 self: *const Ledger,
359 active: ?Slot,
360 generation: schema.GenerationId,
361 ) (ValidationError || Exhaustion)!void {
362 if (active) |slot| {
363 const current = self.slotHeader(slot).generation;
364 if (current == std.math.maxInt(schema.GenerationId)) {
365 return error.GenerationExhausted;
366 }
367 if (generation != current + 1) {
368 return error.GenerationOutOfOrder;
369 }
370 return;
371 }
372 if (generation == 0) return error.GenerationOutOfOrder;
373 }
374
375 fn requireOwned(self: *const Ledger) OwnerError!void {
376 const header = self.sharedHeaderConst();
377 if (self.phase != .steady or header.phase != .steady) {
378 return error.InvalidPhase;
379 }
380 std.debug.assert(header.magic == header_magic);
381 if (header.self_address != @intFromPtr(self)) {
382 return error.CopiedOwner;
383 }
384 }
385
386 fn sharedHeader(self: *Ledger) *Header {
387 return pointerAt(
388 Header,
389 self.storage,
390 self.capacity.layout.header,
391 );
392 }
393
394 fn sharedHeaderConst(self: *const Ledger) *const Header {
395 return constPointerAt(
396 Header,
397 self.storage,
398 self.capacity.layout.header,
399 );
400 }
401
402 fn slotHeader(self: *const Ledger, slot: Slot) *const SlotHeader {
403 return constPointerAt(
404 SlotHeader,
405 self.storage,
406 self.slotLayout(slot).header,
407 );
408 }
409
410 fn slotLayout(self: *const Ledger, slot: Slot) limits.SlotLayout {
411 return self.capacity.layout.slots[@backingInt(slot)];
412 }
413
414 fn loadSlots(self: *const Ledger) u8 {
415 return @atomicLoad(
416 u8,
417 &self.sharedHeaderConst().slot_state,
418 .acquire,
419 );
420 }
421
422 fn storeSlots(self: *Ledger, state: u8) void {
423 @atomicStore(
424 u8,
425 &self.sharedHeader().slot_state,
426 state,
427 .release,
428 );
429 }
430 };
431
432 fn validateOccupancy(
433 bounded: limits.Capacity,
434 input: schema.GenerationInput,
435 ) Ledger.Exhaustion!void {
436 if (input.nodes.len > bounded.nodes) return error.NodeFull;
437 if (input.edges.len > bounded.edges) return error.EdgeFull;
438 if (input.artifacts.len > bounded.artifacts) return error.ArtifactFull;
439 if (input.ranges.len > bounded.ranges) return error.RangeFull;
440 if (input.digests.len > bounded.digests) return error.DigestFull;
441 if (input.provenance_parents.len > bounded.provenance_parents) {
442 return error.ProvenanceParentFull;
443 }
444 if (input.source_records.len > bounded.source_records) {
445 return error.SourceRecordFull;
446 }
447 if (input.build_records.len > bounded.build_records) {
448 return error.BuildRecordFull;
449 }
450 if (input.authorities.len > bounded.authorities) {
451 return error.AuthorityFull;
452 }
453 if (input.lineage_references.len > bounded.lineage_references) {
454 return error.LineageReferenceFull;
455 }
456 if (input.service_descriptors.len > bounded.service_descriptors) {
457 return error.ServiceDescriptorFull;
458 }
459 if (input.residual_roots.len > bounded.residual_roots) {
460 return error.ResidualRootFull;
461 }
462 if (input.claims.len > bounded.claims) return error.ClaimFull;
463 if (input.claim_nodes.len > bounded.claim_nodes) {
464 return error.ClaimNodeFull;
465 }
466 }
467
468 fn validateCanonical(input: schema.GenerationInput) ValidationError!void {
469 if (!digestKnown(&input.root)) return error.UnknownGenerationRoot;
470 try validateNodes(input.nodes);
471 try validateEdges(input.edges, input.nodes.len);
472 try validateArtifacts(input.artifacts, input.nodes.len);
473 try validateRanges(input.ranges, input.artifacts);
474 try validateDigests(input.digests, input.nodes.len);
475 try validateProvenance(input.provenance_parents, input.nodes.len);
476 try validateSources(input.source_records, input.nodes.len);
477 try validateBuilds(input.build_records, input.nodes.len);
478 try validateAuthorities(input.authorities, input.nodes.len);
479 try validateLineage(input.lineage_references, input.nodes.len);
480 try validateServices(input.service_descriptors, input.nodes.len);
481 try validateClaims(input.claims, input.artifacts);
482 try validateResiduals(
483 input.residual_roots,
484 input.nodes.len,
485 input.claims.len,
486 );
487 try validateClaimNodes(
488 input.claim_nodes,
489 input.nodes.len,
490 input.claims.len,
491 );
492 const expected = closure.canonical.generation(input);
493 if (!input.root.eql(&expected)) return error.GenerationRootMismatch;
494 }
495
496 fn validateNodes(nodes: []const schema.Node) ValidationError!void {
497 for (nodes, 0..) |*node, index| {
498 if (!canonicalId(node.id, index) or
499 !textCanonical(&node.descriptor) or
500 !digestKnown(&node.identity) or
501 !phaseSetCanonical(node.phases) or
502 !textCanonical(&node.owner))
503 {
504 return error.NonCanonicalNode;
505 }
506 }
507 }
508
509 fn validateEdges(
510 edges: []const schema.Edge,
511 nodes_len: usize,
512 ) ValidationError!void {
513 for (edges, 0..) |edge, index| {
514 if (!canonicalId(edge.id, index)) {
515 return error.NonCanonicalEdge;
516 }
517 if (!referenceValid(edge.source, nodes_len) or
518 !referenceValid(edge.target, nodes_len))
519 {
520 return error.UnknownNodeReference;
521 }
522 }
523 }
524
525 fn validateArtifacts(
526 artifacts: []const schema.Artifact,
527 nodes_len: usize,
528 ) ValidationError!void {
529 for (artifacts, 0..) |*artifact, index| {
530 if (!canonicalId(artifact.id, index) or
531 artifact.byte_length == 0 or
532 !digestKnown(&artifact.digest))
533 {
534 return error.NonCanonicalArtifact;
535 }
536 if (!referenceValid(artifact.node, nodes_len)) {
537 return error.UnknownNodeReference;
538 }
539 }
540 }
541
542 fn validateRanges(
543 ranges: []const schema.ByteRange,
544 artifacts: []const schema.Artifact,
545 ) ValidationError!void {
546 for (ranges, 0..) |*range, index| {
547 if (!canonicalId(range.id, index) or
548 !digestKnown(&range.digest))
549 {
550 return error.NonCanonicalRange;
551 }
552 if (!referenceValid(range.artifact, artifacts.len)) {
553 return error.UnknownArtifactReference;
554 }
555 const artifact = artifacts[range.artifact - 1];
556 const end = std.math.add(u64, range.offset, range.length) catch {
557 return error.InvalidByteRange;
558 };
559 if (range.length == 0 or end > artifact.byte_length) {
560 return error.InvalidByteRange;
561 }
562 }
563 }
564
565 fn validateDigests(
566 digests: []const schema.DigestRecord,
567 nodes_len: usize,
568 ) ValidationError!void {
569 for (digests, 0..) |*digest, index| {
570 if (!canonicalId(digest.id, index) or
571 !textCanonical(&digest.purpose) or
572 !digestKnown(&digest.digest))
573 {
574 return error.NonCanonicalDigest;
575 }
576 if (!referenceValid(digest.node, nodes_len)) {
577 return error.UnknownNodeReference;
578 }
579 }
580 }
581
582 fn validateProvenance(
583 parents: []const schema.ProvenanceParent,
584 nodes_len: usize,
585 ) ValidationError!void {
586 for (parents, 0..) |parent, index| {
587 if (!canonicalId(parent.id, index)) {
588 return error.NonCanonicalProvenanceParent;
589 }
590 if (!referenceValid(parent.child, nodes_len) or
591 !referenceValid(parent.parent, nodes_len))
592 {
593 return error.UnknownNodeReference;
594 }
595 }
596 }
597
598 fn validateSources(
599 records: []const schema.SourceRecord,
600 nodes_len: usize,
601 ) ValidationError!void {
602 for (records, 0..) |*record, index| {
603 if (!canonicalId(record.id, index) or
604 !textCanonical(&record.path) or
605 !digestKnown(&record.digest))
606 {
607 return error.NonCanonicalSourceRecord;
608 }
609 if (!referenceValid(record.node, nodes_len)) {
610 return error.UnknownNodeReference;
611 }
612 }
613 }
614
615 fn validateBuilds(
616 records: []const schema.BuildRecord,
617 nodes_len: usize,
618 ) ValidationError!void {
619 for (records, 0..) |*record, index| {
620 if (!canonicalId(record.id, index) or
621 !textCanonical(&record.option) or
622 !digestKnown(&record.digest))
623 {
624 return error.NonCanonicalBuildRecord;
625 }
626 if (!referenceValid(record.node, nodes_len) or
627 !referenceValid(record.tool, nodes_len))
628 {
629 return error.UnknownNodeReference;
630 }
631 }
632 }
633
634 fn validateAuthorities(
635 records: []const schema.Authority,
636 nodes_len: usize,
637 ) ValidationError!void {
638 for (records, 0..) |record, index| {
639 if (!canonicalId(record.id, index)) {
640 return error.NonCanonicalAuthority;
641 }
642 if (!referenceValid(record.source, nodes_len) or
643 !referenceValid(record.target, nodes_len))
644 {
645 return error.UnknownNodeReference;
646 }
647 }
648 }
649
650 fn validateLineage(
651 records: []const schema.LineageReference,
652 nodes_len: usize,
653 ) ValidationError!void {
654 for (records, 0..) |*record, index| {
655 if (!canonicalId(record.id, index) or
656 !textCanonical(&record.descriptor) or
657 !digestKnown(&record.digest))
658 {
659 return error.NonCanonicalLineageReference;
660 }
661 if (!referenceValid(record.node, nodes_len)) {
662 return error.UnknownNodeReference;
663 }
664 }
665 }
666
667 fn validateServices(
668 records: []const schema.ServiceDescriptor,
669 nodes_len: usize,
670 ) ValidationError!void {
671 for (records, 0..) |*record, index| {
672 if (!canonicalId(record.id, index) or
673 !textCanonical(&record.provider) or
674 !textCanonical(&record.protocol) or
675 !textCanonical(&record.endpoint_rule) or
676 !textCanonical(&record.trust_anchor) or
677 !textCanonical(&record.failure_contract))
678 {
679 return error.NonCanonicalServiceDescriptor;
680 }
681 if (!referenceValid(record.node, nodes_len)) {
682 return error.UnknownNodeReference;
683 }
684 }
685 }
686
687 fn validateResiduals(
688 records: []const schema.ResidualRoot,
689 nodes_len: usize,
690 claims_len: usize,
691 ) ValidationError!void {
692 for (records, 0..) |record, index| {
693 if (!canonicalId(record.id, index)) {
694 return error.NonCanonicalResidualRoot;
695 }
696 if (!referenceValid(record.node, nodes_len)) {
697 return error.UnknownNodeReference;
698 }
699 if (!referenceValid(record.claim, claims_len)) {
700 return error.UnknownClaimReference;
701 }
702 }
703 }
704
705 fn validateClaims(
706 claims: []const schema.Claim,
707 artifacts: []const schema.Artifact,
708 ) ValidationError!void {
709 for (claims, 0..) |*claim, index| {
710 if (!canonicalId(claim.id, index) or
711 !textCanonical(&claim.name) or
712 !digestKnown(&claim.artifact_digest) or
713 !edgeSetCanonical(claim.traversed_edges))
714 {
715 return error.NonCanonicalClaim;
716 }
717 if (!referenceValid(claim.artifact, artifacts.len)) {
718 return error.UnknownArtifactReference;
719 }
720 if (!claim.artifact_digest.eql(
721 &artifacts[claim.artifact - 1].digest,
722 )) {
723 return error.NonCanonicalClaim;
724 }
725 if (!profilesCanonical(&claim.profiles)) {
726 return error.UnknownProfile;
727 }
728 }
729 }
730
731 fn validateClaimNodes(
732 records: []const schema.ClaimNode,
733 nodes_len: usize,
734 claims_len: usize,
735 ) ValidationError!void {
736 for (records, 0..) |record, index| {
737 if (!canonicalId(record.id, index)) {
738 return error.NonCanonicalClaimNode;
739 }
740 if (!referenceValid(record.node, nodes_len)) {
741 return error.UnknownNodeReference;
742 }
743 if (!referenceValid(record.claim, claims_len)) {
744 return error.UnknownClaimReference;
745 }
746 }
747 }
748
749 fn profilesCanonical(profiles: *const schema.ProfileSet) bool {
750 return profileCanonical(&profiles.executable) and
751 profileCanonical(&profiles.service_trust) and
752 profileCanonical(&profiles.model_origin) and
753 profileCanonical(&profiles.bootstrap);
754 }
755
756 fn profileCanonical(profile: *const schema.ProfileRef) bool {
757 if (profile.required) {
758 return textCanonical(&profile.id) and
759 textCanonical(&profile.source) and
760 digestKnown(&profile.body_sha256);
761 }
762 return textAbsent(&profile.id) and
763 textAbsent(&profile.source) and
764 !digestKnown(&profile.body_sha256);
765 }
766
767 fn textCanonical(text: anytype) bool {
768 const length: usize = text.len;
769 if (length == 0 or length > text.bytes.len) return false;
770 for (text.bytes[0..length]) |byte| {
771 if (byte < 0x20 or byte == 0x7f) return false;
772 }
773 for (text.bytes[length..]) |byte| {
774 if (byte != 0) return false;
775 }
776 return true;
777 }
778
779 fn textAbsent(text: anytype) bool {
780 if (text.len != 0) return false;
781 for (text.bytes) |byte| {
782 if (byte != 0) return false;
783 }
784 return true;
785 }
786
787 fn digestKnown(digest: *const schema.Digest) bool {
788 for (digest.bytes) |byte| {
789 if (byte != 0) return true;
790 }
791 return false;
792 }
793
794 fn phaseSetCanonical(phases: schema.PhaseSet) bool {
795 const all = (@as(schema.PhaseSet, 1) <<
796 std.meta.tags(schema.Phase).len) - 1;
797 return phases != 0 and phases & ~all == 0;
798 }
799
800 fn edgeSetCanonical(edges: schema.EdgeSet) bool {
801 const all = (@as(schema.EdgeSet, 1) <<
802 std.meta.tags(schema.EdgeKind).len) - 1;
803 return edges & ~all == 0;
804 }
805
806 fn canonicalId(id: u32, index: usize) bool {
807 return id == @as(u32, @intCast(index + 1));
808 }
809
810 fn referenceValid(id: u32, count: usize) bool {
811 return id != 0 and id <= count;
812 }
813
814 fn copyInput(
815 storage: Ledger.Storage,
816 layout: limits.SlotLayout,
817 input: schema.GenerationInput,
818 ) void {
819 copyAt(schema.Node, storage, layout.nodes, input.nodes);
820 copyAt(schema.Edge, storage, layout.edges, input.edges);
821 copyAt(schema.Artifact, storage, layout.artifacts, input.artifacts);
822 copyAt(schema.ByteRange, storage, layout.ranges, input.ranges);
823 copyAt(schema.DigestRecord, storage, layout.digests, input.digests);
824 copyAt(
825 schema.ProvenanceParent,
826 storage,
827 layout.provenance_parents,
828 input.provenance_parents,
829 );
830 copyAt(
831 schema.SourceRecord,
832 storage,
833 layout.source_records,
834 input.source_records,
835 );
836 copyAt(
837 schema.BuildRecord,
838 storage,
839 layout.build_records,
840 input.build_records,
841 );
842 copyAt(
843 schema.Authority,
844 storage,
845 layout.authorities,
846 input.authorities,
847 );
848 copyAt(
849 schema.LineageReference,
850 storage,
851 layout.lineage_references,
852 input.lineage_references,
853 );
854 copyAt(
855 schema.ServiceDescriptor,
856 storage,
857 layout.service_descriptors,
858 input.service_descriptors,
859 );
860 copyAt(
861 schema.ResidualRoot,
862 storage,
863 layout.residual_roots,
864 input.residual_roots,
865 );
866 copyAt(schema.Claim, storage, layout.claims, input.claims);
867 copyAt(
868 schema.ClaimNode,
869 storage,
870 layout.claim_nodes,
871 input.claim_nodes,
872 );
873 pointerAt(SlotHeader, storage, layout.header).* = slotHeader(input);
874 }
875
876 fn slotHeader(input: schema.GenerationInput) SlotHeader {
877 return .{
878 .generation = input.id,
879 .root = input.root,
880 .counts = .{
881 .nodes = @intCast(input.nodes.len),
882 .edges = @intCast(input.edges.len),
883 .artifacts = @intCast(input.artifacts.len),
884 .ranges = @intCast(input.ranges.len),
885 .digests = @intCast(input.digests.len),
886 .provenance_parents = @intCast(
887 input.provenance_parents.len,
888 ),
889 .source_records = @intCast(input.source_records.len),
890 .build_records = @intCast(input.build_records.len),
891 .authorities = @intCast(input.authorities.len),
892 .lineage_references = @intCast(
893 input.lineage_references.len,
894 ),
895 .service_descriptors = @intCast(
896 input.service_descriptors.len,
897 ),
898 .residual_roots = @intCast(input.residual_roots.len),
899 .claims = @intCast(input.claims.len),
900 .claim_nodes = @intCast(input.claim_nodes.len),
901 },
902 .reserved = @splat(0),
903 };
904 }
905
906 fn inputView(
907 storage: Ledger.Storage,
908 layout: limits.SlotLayout,
909 ) schema.GenerationInput {
910 const header = constPointerAt(SlotHeader, storage, layout.header);
911 return .{
912 .id = header.generation,
913 .root = header.root,
914 .nodes = constSliceAt(schema.Node, storage, layout.nodes, header.counts.nodes),
915 .edges = constSliceAt(schema.Edge, storage, layout.edges, header.counts.edges),
916 .artifacts = constSliceAt(
917 schema.Artifact,
918 storage,
919 layout.artifacts,
920 header.counts.artifacts,
921 ),
922 .ranges = constSliceAt(schema.ByteRange, storage, layout.ranges, header.counts.ranges),
923 .digests = constSliceAt(
924 schema.DigestRecord,
925 storage,
926 layout.digests,
927 header.counts.digests,
928 ),
929 .provenance_parents = provenanceView(storage, layout, header),
930 .source_records = sourceView(storage, layout, header),
931 .build_records = buildView(storage, layout, header),
932 .authorities = constSliceAt(
933 schema.Authority,
934 storage,
935 layout.authorities,
936 header.counts.authorities,
937 ),
938 .lineage_references = lineageView(storage, layout, header),
939 .service_descriptors = serviceView(storage, layout, header),
940 .residual_roots = constSliceAt(
941 schema.ResidualRoot,
942 storage,
943 layout.residual_roots,
944 header.counts.residual_roots,
945 ),
946 .claims = constSliceAt(
947 schema.Claim,
948 storage,
949 layout.claims,
950 header.counts.claims,
951 ),
952 .claim_nodes = constSliceAt(
953 schema.ClaimNode,
954 storage,
955 layout.claim_nodes,
956 header.counts.claim_nodes,
957 ),
958 };
959 }
960
961 fn provenanceView(
962 storage: Ledger.Storage,
963 layout: limits.SlotLayout,
964 header: *const SlotHeader,
965 ) []const schema.ProvenanceParent {
966 return constSliceAt(
967 schema.ProvenanceParent,
968 storage,
969 layout.provenance_parents,
970 header.counts.provenance_parents,
971 );
972 }
973
974 fn sourceView(
975 storage: Ledger.Storage,
976 layout: limits.SlotLayout,
977 header: *const SlotHeader,
978 ) []const schema.SourceRecord {
979 return constSliceAt(
980 schema.SourceRecord,
981 storage,
982 layout.source_records,
983 header.counts.source_records,
984 );
985 }
986
987 fn buildView(
988 storage: Ledger.Storage,
989 layout: limits.SlotLayout,
990 header: *const SlotHeader,
991 ) []const schema.BuildRecord {
992 return constSliceAt(
993 schema.BuildRecord,
994 storage,
995 layout.build_records,
996 header.counts.build_records,
997 );
998 }
999
1000 fn lineageView(
1001 storage: Ledger.Storage,
1002 layout: limits.SlotLayout,
1003 header: *const SlotHeader,
1004 ) []const schema.LineageReference {
1005 return constSliceAt(
1006 schema.LineageReference,
1007 storage,
1008 layout.lineage_references,
1009 header.counts.lineage_references,
1010 );
1011 }
1012
1013 fn serviceView(
1014 storage: Ledger.Storage,
1015 layout: limits.SlotLayout,
1016 header: *const SlotHeader,
1017 ) []const schema.ServiceDescriptor {
1018 return constSliceAt(
1019 schema.ServiceDescriptor,
1020 storage,
1021 layout.service_descriptors,
1022 header.counts.service_descriptors,
1023 );
1024 }
1025
1026 fn copyAt(
1027 comptime T: type,
1028 storage: Ledger.Storage,
1029 offset: usize,
1030 source: []const T,
1031 ) void {
1032 const destination = sliceAt(T, storage, offset, source.len);
1033 std.mem.copyForwards(T, destination, source);
1034 }
1035
1036 fn sliceAt(
1037 comptime T: type,
1038 storage: Ledger.Storage,
1039 offset: usize,
1040 count: usize,
1041 ) []T {
1042 std.debug.assert(offset <= storage.len);
1043 std.debug.assert(count <= (storage.len - offset) / @sizeOf(T));
1044 const pointer: [*]T = @ptrCast(@alignCast(storage[offset..].ptr));
1045 return pointer[0..count];
1046 }
1047
1048 fn constSliceAt(
1049 comptime T: type,
1050 storage: Ledger.Storage,
1051 offset: usize,
1052 count: usize,
1053 ) []const T {
1054 std.debug.assert(offset <= storage.len);
1055 std.debug.assert(count <= (storage.len - offset) / @sizeOf(T));
1056 const pointer: [*]const T = @ptrCast(
1057 @alignCast(storage[offset..].ptr),
1058 );
1059 return pointer[0..count];
1060 }
1061
1062 fn pointerAt(
1063 comptime T: type,
1064 storage: Ledger.Storage,
1065 offset: usize,
1066 ) *T {
1067 std.debug.assert(offset + @sizeOf(T) <= storage.len);
1068 return @ptrCast(@alignCast(storage[offset..].ptr));
1069 }
1070
1071 fn constPointerAt(
1072 comptime T: type,
1073 storage: Ledger.Storage,
1074 offset: usize,
1075 ) *const T {
1076 std.debug.assert(offset + @sizeOf(T) <= storage.len);
1077 return @ptrCast(@alignCast(storage[offset..].ptr));
1078 }
1079
1080 fn inactiveSlot(active: ?Slot) Slot {
1081 return if (active) |slot| switch (slot) {
1082 .first => .second,
1083 .second => .first,
1084 } else .first;
1085 }
1086
1087 fn encodeSlots(active: ?Slot, staged: ?Slot) u8 {
1088 if (active != null and active == staged) unreachable;
1089 return slotCode(active) | (slotCode(staged) << 2);
1090 }
1091
1092 fn activeSlot(state: u8) ?Slot {
1093 return decodeSlot(state & slot_mask);
1094 }
1095
1096 fn stagedSlot(state: u8) ?Slot {
1097 return decodeSlot((state >> 2) & slot_mask);
1098 }
1099
1100 fn slotCode(slot: ?Slot) u8 {
1101 return if (slot) |value| @backingInt(value) else slot_none;
1102 }
1103
1104 fn decodeSlot(code: u8) ?Slot {
1105 return switch (code) {
1106 0 => .first,
1107 1 => .second,
1108 slot_none => null,
1109 else => unreachable,
1110 };
1111 }
1112
1113 const TestCounts = struct {
1114 nodes: usize = 2,
1115 edges: usize = 1,
1116 artifacts: usize = 1,
1117 ranges: usize = 1,
1118 digests: usize = 1,
1119 provenance_parents: usize = 1,
1120 source_records: usize = 1,
1121 build_records: usize = 1,
1122 authorities: usize = 1,
1123 lineage_references: usize = 1,
1124 service_descriptors: usize = 1,
1125 residual_roots: usize = 1,
1126 claims: usize = 1,
1127 claim_nodes: usize = 1,
1128 };
1129
1130 const test_limits = Ledger.Limits{
1131 .nodes = 2,
1132 .edges = 1,
1133 .artifacts = 1,
1134 .ranges = 1,
1135 .digests = 1,
1136 .provenance_parents = 1,
1137 .source_records = 1,
1138 .build_records = 1,
1139 .authorities = 1,
1140 .lineage_references = 1,
1141 .service_descriptors = 1,
1142 .residual_roots = 1,
1143 .claims = 1,
1144 .claim_nodes = 1,
1145 .receipt_bytes = 1,
1146 };
1147
1148 const test_capacity = Ledger.Capacity.derive(test_limits) catch unreachable;
1149 const test_storage_bytes = test_capacity.storage_bytes;
1150
1151 const Fixture = struct {
1152 nodes: [3]schema.Node,
1153 edges: [2]schema.Edge,
1154 artifacts: [2]schema.Artifact,
1155 ranges: [2]schema.ByteRange,
1156 digests: [2]schema.DigestRecord,
1157 provenance_parents: [2]schema.ProvenanceParent,
1158 source_records: [2]schema.SourceRecord,
1159 build_records: [2]schema.BuildRecord,
1160 authorities: [2]schema.Authority,
1161 lineage_references: [2]schema.LineageReference,
1162 service_descriptors: [2]schema.ServiceDescriptor,
1163 residual_roots: [2]schema.ResidualRoot,
1164 claims: [2]schema.Claim,
1165 claim_nodes: [2]schema.ClaimNode,
1166
1167 fn init() Fixture {
1168 return .{
1169 .nodes = .{
1170 testNode(1, 0x11),
1171 testNode(2, 0x22),
1172 testNode(3, 0x33),
1173 },
1174 .edges = .{
1175 .{ .id = 1, .source = 1, .target = 2, .kind = .derivation },
1176 .{ .id = 2, .source = 2, .target = 1, .kind = .validation },
1177 },
1178 .artifacts = .{
1179 testArtifact(1, 1, 0x41),
1180 testArtifact(2, 2, 0x42),
1181 },
1182 .ranges = .{
1183 testRange(1, 1, 0x51),
1184 testRange(2, 2, 0x52),
1185 },
1186 .digests = .{
1187 testDigestRecord(1, 1, 0x61),
1188 testDigestRecord(2, 2, 0x62),
1189 },
1190 .provenance_parents = .{
1191 .{ .id = 1, .child = 1, .parent = 2 },
1192 .{ .id = 2, .child = 2, .parent = 1 },
1193 },
1194 .source_records = .{
1195 testSource(1, 1, 0x71),
1196 testSource(2, 2, 0x72),
1197 },
1198 .build_records = .{
1199 testBuild(1, 1, 2, 0x81),
1200 testBuild(2, 2, 1, 0x82),
1201 },
1202 .authorities = .{
1203 .{ .id = 1, .source = 1, .target = 2, .granted = .memory },
1204 .{ .id = 2, .source = 2, .target = 1, .granted = .validation },
1205 },
1206 .lineage_references = .{
1207 testLineage(1, 1, 0x91),
1208 testLineage(2, 2, 0x92),
1209 },
1210 .service_descriptors = .{
1211 testService(1, 1),
1212 testService(2, 2),
1213 },
1214 .residual_roots = .{
1215 .{ .id = 1, .claim = 1, .node = 1, .treatment = .verified },
1216 .{ .id = 2, .claim = 1, .node = 2, .treatment = .owned },
1217 },
1218 .claims = .{
1219 testClaim(1, 1, 0x41),
1220 testClaim(2, 2, 0x42),
1221 },
1222 .claim_nodes = .{
1223 .{ .id = 1, .claim = 1, .node = 1, .treatment = .owned },
1224 .{ .id = 2, .claim = 1, .node = 2, .treatment = .verified },
1225 },
1226 };
1227 }
1228
1229 fn input(
1230 self: *const Fixture,
1231 generation: schema.GenerationId,
1232 counts: TestCounts,
1233 ) schema.GenerationInput {
1234 var manifest = schema.GenerationInput{
1235 .id = generation,
1236 .root = schema.Digest.zero(),
1237 .nodes = self.nodes[0..counts.nodes],
1238 .edges = self.edges[0..counts.edges],
1239 .artifacts = self.artifacts[0..counts.artifacts],
1240 .ranges = self.ranges[0..counts.ranges],
1241 .digests = self.digests[0..counts.digests],
1242 .provenance_parents = self.provenance_parents[0..counts.provenance_parents],
1243 .source_records = self.source_records[0..counts.source_records],
1244 .build_records = self.build_records[0..counts.build_records],
1245 .authorities = self.authorities[0..counts.authorities],
1246 .lineage_references = self.lineage_references[0..counts.lineage_references],
1247 .service_descriptors = self.service_descriptors[0..counts.service_descriptors],
1248 .residual_roots = self.residual_roots[0..counts.residual_roots],
1249 .claims = self.claims[0..counts.claims],
1250 .claim_nodes = self.claim_nodes[0..counts.claim_nodes],
1251 };
1252 manifest.root = closure.canonical.generation(manifest);
1253 return manifest;
1254 }
1255 };
1256
1257 fn testNode(id: schema.NodeId, byte: u8) schema.Node {
1258 return .{
1259 .id = id,
1260 .descriptor = testDescriptor("node"),
1261 .identity = testDigest(byte),
1262 .subject_kind = .binary,
1263 .material_role = .platform,
1264 .origin = .owned_derivation,
1265 .phases = schema.phaseBit(.shipment),
1266 .execution_locus = .normal_world_cpu,
1267 .owner = testName("closure"),
1268 .authority = .platform_control,
1269 .artifact_kind = .executable,
1270 };
1271 }
1272
1273 fn testArtifact(
1274 id: schema.RecordId,
1275 node: schema.NodeId,
1276 byte: u8,
1277 ) schema.Artifact {
1278 return .{
1279 .id = id,
1280 .node = node,
1281 .byte_length = 16,
1282 .digest = testDigest(byte),
1283 .witness = .owned,
1284 };
1285 }
1286
1287 fn testRange(
1288 id: schema.RecordId,
1289 artifact: schema.RecordId,
1290 byte: u8,
1291 ) schema.ByteRange {
1292 return .{
1293 .id = id,
1294 .artifact = artifact,
1295 .offset = 0,
1296 .length = 16,
1297 .digest = testDigest(byte),
1298 .executable = true,
1299 .witness = .owned,
1300 };
1301 }
1302
1303 fn testDigestRecord(
1304 id: schema.RecordId,
1305 node: schema.NodeId,
1306 byte: u8,
1307 ) schema.DigestRecord {
1308 return .{
1309 .id = id,
1310 .node = node,
1311 .purpose = testName("identity"),
1312 .digest = testDigest(byte),
1313 .witness = .owned,
1314 };
1315 }
1316
1317 fn testSource(
1318 id: schema.RecordId,
1319 node: schema.NodeId,
1320 byte: u8,
1321 ) schema.SourceRecord {
1322 return .{
1323 .id = id,
1324 .node = node,
1325 .path = testDescriptor("lib/closure"),
1326 .digest = testDigest(byte),
1327 .witness = .owned,
1328 };
1329 }
1330
1331 fn testBuild(
1332 id: schema.RecordId,
1333 node: schema.NodeId,
1334 tool: schema.NodeId,
1335 byte: u8,
1336 ) schema.BuildRecord {
1337 return .{
1338 .id = id,
1339 .node = node,
1340 .tool = tool,
1341 .option = testDescriptor("-OReleaseSafe"),
1342 .digest = testDigest(byte),
1343 .witness = .owned,
1344 };
1345 }
1346
1347 fn testLineage(
1348 id: schema.RecordId,
1349 node: schema.NodeId,
1350 byte: u8,
1351 ) schema.LineageReference {
1352 return .{
1353 .id = id,
1354 .node = node,
1355 .descriptor = testDescriptor("owned lineage"),
1356 .digest = testDigest(byte),
1357 .witness = .owned,
1358 };
1359 }
1360
1361 fn testService(
1362 id: schema.RecordId,
1363 node: schema.NodeId,
1364 ) schema.ServiceDescriptor {
1365 return .{
1366 .id = id,
1367 .node = node,
1368 .provider = testDescriptor("provider"),
1369 .protocol = testDescriptor("protocol"),
1370 .endpoint_rule = testDescriptor("fixed endpoint"),
1371 .trust_anchor = testDescriptor("owned anchor"),
1372 .failure_contract = testDescriptor("typed failure"),
1373 .requirement = .required,
1374 };
1375 }
1376
1377 fn testClaim(
1378 id: schema.ClaimId,
1379 artifact: schema.RecordId,
1380 byte: u8,
1381 ) schema.Claim {
1382 return .{
1383 .id = id,
1384 .name = testName("owned"),
1385 .artifact = artifact,
1386 .artifact_digest = testDigest(byte),
1387 .profiles = absentProfiles(),
1388 .traversed_edges = schema.edgeBit(.derivation),
1389 };
1390 }
1391
1392 fn absentProfiles() schema.ProfileSet {
1393 return .{
1394 .executable = schema.ProfileRef.absent(),
1395 .service_trust = schema.ProfileRef.absent(),
1396 .model_origin = schema.ProfileRef.absent(),
1397 .bootstrap = schema.ProfileRef.absent(),
1398 };
1399 }
1400
1401 fn testName(input: []const u8) schema.Name {
1402 return schema.Name.init(input) catch unreachable;
1403 }
1404
1405 fn testDescriptor(input: []const u8) schema.Descriptor {
1406 return schema.Descriptor.init(input) catch unreachable;
1407 }
1408
1409 fn testDigest(byte: u8) schema.Digest {
1410 return .{ .bytes = @splat(byte) };
1411 }
1412
1413 fn initializeTestLedger(
1414 ledger: *Ledger,
1415 storage: Ledger.Storage,
1416 ) !void {
1417 ledger.* = try Ledger.init(storage, test_limits);
1418 ledger.activate();
1419 }
1420
1421 fn expectAtomicPrepareError(
1422 expected: anytype,
1423 ledger: *Ledger,
1424 input: schema.GenerationInput,
1425 storage: *[test_storage_bytes]u8,
1426 ) !void {
1427 const before = storage.*;
1428 try std.testing.expectError(expected, ledger.prepare(input));
1429 try std.testing.expectEqualSlices(u8, &before, storage);
1430 }
1431
1432 test "closure ledger accepts exact and extra storage" {
1433 comptime {
1434 @stardustClaim(
1435 @import("capacity").witness(Ledger, "closure_ledger_capacity_capacity_model"),
1436 null,
1437 null,
1438 null,
1439 null,
1440 null,
1441 null,
1442 );
1443 }
1444 comptime {
1445 @stardustClaim(
1446 @import("capacity").witness(Ledger, "closure_ledger_capacity_transitive_risk"),
1447 null,
1448 null,
1449 null,
1450 null,
1451 null,
1452 null,
1453 );
1454 }
1455
1456 var short: [test_storage_bytes - 1]u8 align(Ledger.storage_alignment) =
1457 @splat(0xa5);
1458 const short_before = short;
1459 try std.testing.expectError(
1460 error.StorageTooShort,
1461 Ledger.init(&short, test_limits),
1462 );
1463 try std.testing.expectEqualSlices(u8, &short_before, &short);
1464 var exact: [test_storage_bytes]u8 align(Ledger.storage_alignment) =
1465 undefined;
1466 var ledger = try Ledger.init(&exact, test_limits);
1467 ledger.activate();
1468 try std.testing.expect((try ledger.snapshot()) == null);
1469 try std.testing.expectEqual(test_storage_bytes, ledger.deinit().len);
1470 var extra: [test_storage_bytes + 1]u8 align(Ledger.storage_alignment) =
1471 undefined;
1472 var extra_ledger = try Ledger.init(&extra, test_limits);
1473 extra_ledger.activate();
1474 try std.testing.expectEqual(extra.len, extra_ledger.deinit().len);
1475 }
1476
1477 test "closure ledger exposes disjoint bounded projection and receipt scratch" {
1478 var storage: [test_storage_bytes]u8 align(Ledger.storage_alignment) =
1479 undefined;
1480 var ledger = try Ledger.init(&storage, test_limits);
1481 ledger.activate();
1482 const scratch = try ledger.projectionScratch();
1483 const receipt = try ledger.receiptScratch();
1484 try std.testing.expectEqual(test_capacity.nodes, scratch.visited.len);
1485 try std.testing.expectEqual(test_capacity.nodes, scratch.queue.len);
1486 try std.testing.expectEqual(test_capacity.receipt_bytes, receipt.len);
1487 try std.testing.expect(
1488 @intFromPtr(scratch.visited.ptr) + scratch.visited.len <=
1489 @intFromPtr(scratch.queue.ptr),
1490 );
1491 try std.testing.expect(
1492 @intFromPtr(scratch.queue.ptr) +
1493 scratch.queue.len * @sizeOf(u32) <=
1494 @intFromPtr(receipt.ptr),
1495 );
1496 _ = ledger.deinit();
1497 }
1498
1499 test "closure ledger admits every occupancy at capacity" {
1500 var storage: [test_storage_bytes]u8 align(Ledger.storage_alignment) =
1501 undefined;
1502 var ledger: Ledger = undefined;
1503 try initializeTestLedger(&ledger, &storage);
1504 var fixture = Fixture.init();
1505 try ledger.prepare(fixture.input(7, .{}));
1506 try ledger.publish();
1507 const snapshot = (try ledger.snapshot()).?;
1508 inline for (@typeInfo(TestCounts).@"struct".field_names) |field| {
1509 try std.testing.expectEqual(
1510 @field(TestCounts{}, field),
1511 @field(snapshot, field).len,
1512 );
1513 }
1514 _ = ledger.deinit();
1515 }
1516
1517 test "closure ledger rejects every occupancy maximum plus one atomically" {
1518 comptime {
1519 @stardustClaim(
1520 @import("capacity").witness(Ledger, "closure_ledger_overload"),
1521 null,
1522 null,
1523 null,
1524 null,
1525 null,
1526 null,
1527 );
1528 }
1529
1530 inline for (.{
1531 .{ "nodes", error.NodeFull },
1532 .{ "edges", error.EdgeFull },
1533 .{ "artifacts", error.ArtifactFull },
1534 .{ "ranges", error.RangeFull },
1535 .{ "digests", error.DigestFull },
1536 .{ "provenance_parents", error.ProvenanceParentFull },
1537 .{ "source_records", error.SourceRecordFull },
1538 .{ "build_records", error.BuildRecordFull },
1539 .{ "authorities", error.AuthorityFull },
1540 .{ "lineage_references", error.LineageReferenceFull },
1541 .{ "service_descriptors", error.ServiceDescriptorFull },
1542 .{ "residual_roots", error.ResidualRootFull },
1543 .{ "claims", error.ClaimFull },
1544 .{ "claim_nodes", error.ClaimNodeFull },
1545 }) |case| {
1546 var storage: [test_storage_bytes]u8 align(Ledger.storage_alignment) =
1547 undefined;
1548 var ledger: Ledger = undefined;
1549 try initializeTestLedger(&ledger, &storage);
1550 var counts = TestCounts{};
1551 @field(counts, case[0]) += 1;
1552 var fixture = Fixture.init();
1553 try expectAtomicPrepareError(
1554 case[1],
1555 &ledger,
1556 fixture.input(1, counts),
1557 &storage,
1558 );
1559 _ = ledger.deinit();
1560 }
1561 }
1562
1563 test "closure ledger rejects noncanonical table records atomically" {
1564 var storage: [test_storage_bytes]u8 align(Ledger.storage_alignment) =
1565 undefined;
1566 var ledger: Ledger = undefined;
1567 try initializeTestLedger(&ledger, &storage);
1568 inline for (.{
1569 .{ "nodes", error.NonCanonicalNode },
1570 .{ "edges", error.NonCanonicalEdge },
1571 .{ "artifacts", error.NonCanonicalArtifact },
1572 .{ "ranges", error.NonCanonicalRange },
1573 .{ "digests", error.NonCanonicalDigest },
1574 .{ "provenance_parents", error.NonCanonicalProvenanceParent },
1575 .{ "source_records", error.NonCanonicalSourceRecord },
1576 .{ "build_records", error.NonCanonicalBuildRecord },
1577 .{ "authorities", error.NonCanonicalAuthority },
1578 .{ "lineage_references", error.NonCanonicalLineageReference },
1579 .{ "service_descriptors", error.NonCanonicalServiceDescriptor },
1580 .{ "residual_roots", error.NonCanonicalResidualRoot },
1581 .{ "claims", error.NonCanonicalClaim },
1582 .{ "claim_nodes", error.NonCanonicalClaimNode },
1583 }) |case| {
1584 var fixture = Fixture.init();
1585 @field(fixture, case[0])[0].id = 2;
1586 try expectAtomicPrepareError(
1587 case[1],
1588 &ledger,
1589 fixture.input(1, .{}),
1590 &storage,
1591 );
1592 }
1593 _ = ledger.deinit();
1594 }
1595
1596 test "closure ledger rejects invalid canonical references atomically" {
1597 comptime {
1598 @stardustClaim(
1599 @import("capacity").witness(Ledger, "closure_ledger_validation"),
1600 null,
1601 null,
1602 null,
1603 null,
1604 null,
1605 null,
1606 );
1607 }
1608
1609 var storage: [test_storage_bytes]u8 align(Ledger.storage_alignment) =
1610 undefined;
1611 var ledger: Ledger = undefined;
1612 try initializeTestLedger(&ledger, &storage);
1613 var fixture = Fixture.init();
1614 var input = fixture.input(1, .{});
1615 input.root = schema.Digest.zero();
1616 try expectAtomicPrepareError(
1617 error.UnknownGenerationRoot,
1618 &ledger,
1619 input,
1620 &storage,
1621 );
1622 fixture = Fixture.init();
1623 input = fixture.input(1, .{});
1624 input.root = testDigest(0xf5);
1625 try expectAtomicPrepareError(
1626 error.GenerationRootMismatch,
1627 &ledger,
1628 input,
1629 &storage,
1630 );
1631 fixture = Fixture.init();
1632 fixture.edges[0].source = 3;
1633 try expectAtomicPrepareError(
1634 error.UnknownNodeReference,
1635 &ledger,
1636 fixture.input(1, .{}),
1637 &storage,
1638 );
1639 fixture = Fixture.init();
1640 fixture.ranges[0].artifact = 2;
1641 try expectAtomicPrepareError(
1642 error.UnknownArtifactReference,
1643 &ledger,
1644 fixture.input(1, .{}),
1645 &storage,
1646 );
1647 _ = ledger.deinit();
1648 }
1649
1650 test "closure ledger rejects claims profiles and byte ranges atomically" {
1651 var storage: [test_storage_bytes]u8 align(Ledger.storage_alignment) =
1652 undefined;
1653 var ledger: Ledger = undefined;
1654 try initializeTestLedger(&ledger, &storage);
1655 var fixture = Fixture.init();
1656 fixture.residual_roots[0].claim = 2;
1657 try expectAtomicPrepareError(
1658 error.UnknownClaimReference,
1659 &ledger,
1660 fixture.input(1, .{}),
1661 &storage,
1662 );
1663 fixture = Fixture.init();
1664 fixture.ranges[0].length = 0;
1665 try expectAtomicPrepareError(
1666 error.InvalidByteRange,
1667 &ledger,
1668 fixture.input(1, .{}),
1669 &storage,
1670 );
1671 fixture = Fixture.init();
1672 fixture.ranges[0].offset = std.math.maxInt(u64);
1673 fixture.ranges[0].length = 2;
1674 try expectAtomicPrepareError(
1675 error.InvalidByteRange,
1676 &ledger,
1677 fixture.input(1, .{}),
1678 &storage,
1679 );
1680 fixture = Fixture.init();
1681 fixture.ranges[0].length = 17;
1682 try expectAtomicPrepareError(
1683 error.InvalidByteRange,
1684 &ledger,
1685 fixture.input(1, .{}),
1686 &storage,
1687 );
1688 fixture = Fixture.init();
1689 fixture.claims[0].profiles.executable.required = true;
1690 try expectAtomicPrepareError(
1691 error.UnknownProfile,
1692 &ledger,
1693 fixture.input(1, .{}),
1694 &storage,
1695 );
1696 fixture = Fixture.init();
1697 fixture.claims[0].profiles.executable.id = testName("stray");
1698 try expectAtomicPrepareError(
1699 error.UnknownProfile,
1700 &ledger,
1701 fixture.input(1, .{}),
1702 &storage,
1703 );
1704 _ = ledger.deinit();
1705 }
1706
1707 test "closure ledger rejects malformed text masks and bindings atomically" {
1708 var storage: [test_storage_bytes]u8 align(Ledger.storage_alignment) =
1709 undefined;
1710 var ledger: Ledger = undefined;
1711 try initializeTestLedger(&ledger, &storage);
1712 var fixture = Fixture.init();
1713 fixture.nodes[0].owner.bytes[fixture.nodes[0].owner.len] = 0xa5;
1714 try expectAtomicPrepareError(
1715 error.NonCanonicalNode,
1716 &ledger,
1717 fixture.input(1, .{}),
1718 &storage,
1719 );
1720 fixture = Fixture.init();
1721 fixture.nodes[0].phases = 0;
1722 try expectAtomicPrepareError(
1723 error.NonCanonicalNode,
1724 &ledger,
1725 fixture.input(1, .{}),
1726 &storage,
1727 );
1728 fixture = Fixture.init();
1729 fixture.nodes[0].phases = 1 << 15;
1730 try expectAtomicPrepareError(
1731 error.NonCanonicalNode,
1732 &ledger,
1733 fixture.input(1, .{}),
1734 &storage,
1735 );
1736 fixture = Fixture.init();
1737 fixture.claims[0].traversed_edges = 1 << 15;
1738 try expectAtomicPrepareError(
1739 error.NonCanonicalClaim,
1740 &ledger,
1741 fixture.input(1, .{}),
1742 &storage,
1743 );
1744 fixture = Fixture.init();
1745 fixture.claims[0].artifact_digest = testDigest(0xf1);
1746 try expectAtomicPrepareError(
1747 error.NonCanonicalClaim,
1748 &ledger,
1749 fixture.input(1, .{}),
1750 &storage,
1751 );
1752 _ = ledger.deinit();
1753 }
1754
1755 test "closure ledger admits canonical required profiles" {
1756 var storage: [test_storage_bytes]u8 align(Ledger.storage_alignment) =
1757 undefined;
1758 var ledger: Ledger = undefined;
1759 try initializeTestLedger(&ledger, &storage);
1760 var fixture = Fixture.init();
1761 fixture.claims[0].profiles.executable = .{
1762 .required = true,
1763 .id = testName("native-workstation-v1"),
1764 .source = testName("issue-tiny-profile"),
1765 .body_sha256 = testDigest(0xc1),
1766 };
1767 try ledger.prepare(fixture.input(1, .{}));
1768 try ledger.publish();
1769 const snapshot = (try ledger.snapshot()).?;
1770 const expected = closure.canonical.generation(snapshot);
1771 try std.testing.expect(snapshot.root.eql(&expected));
1772 _ = ledger.deinit();
1773 }
1774
1775 test "closure ledger prepare and publish retain the prior generation" {
1776 var storage: [test_storage_bytes]u8 align(Ledger.storage_alignment) =
1777 undefined;
1778 var ledger: Ledger = undefined;
1779 try initializeTestLedger(&ledger, &storage);
1780 var fixture = Fixture.init();
1781 try ledger.prepare(fixture.input(10, .{}));
1782 try std.testing.expect((try ledger.snapshot()) == null);
1783 try ledger.publish();
1784 const first = (try ledger.snapshot()).?;
1785 const first_address = @intFromPtr(first.nodes.ptr);
1786 try ledger.prepare(fixture.input(11, .{}));
1787 try std.testing.expectEqual(
1788 @as(schema.GenerationId, 10),
1789 (try ledger.snapshot()).?.id,
1790 );
1791 try std.testing.expectEqual(
1792 first_address,
1793 @intFromPtr((try ledger.snapshot()).?.nodes.ptr),
1794 );
1795 try ledger.publish();
1796 try std.testing.expectEqual(
1797 @as(schema.GenerationId, 11),
1798 (try ledger.snapshot()).?.id,
1799 );
1800 _ = ledger.deinit();
1801 }
1802
1803 test "closure ledger generation rejection preserves equivalent state" {
1804 var storage: [test_storage_bytes]u8 align(Ledger.storage_alignment) =
1805 undefined;
1806 var ledger: Ledger = undefined;
1807 try initializeTestLedger(&ledger, &storage);
1808 var fixture = Fixture.init();
1809 try ledger.prepare(fixture.input(20, .{}));
1810 try ledger.publish();
1811 try expectAtomicPrepareError(
1812 error.GenerationOutOfOrder,
1813 &ledger,
1814 fixture.input(22, .{}),
1815 &storage,
1816 );
1817 var equivalent = fixture.input(21, .{});
1818 equivalent.root = (try ledger.snapshot()).?.root;
1819 try expectAtomicPrepareError(
1820 error.GenerationRootMismatch,
1821 &ledger,
1822 equivalent,
1823 &storage,
1824 );
1825 try ledger.prepare(fixture.input(21, .{}));
1826 const staged = storage;
1827 try std.testing.expectError(
1828 error.GenerationAlreadyStaged,
1829 ledger.prepare(fixture.input(21, .{})),
1830 );
1831 try std.testing.expectEqualSlices(u8, &staged, &storage);
1832 _ = ledger.deinit();
1833 }
1834
1835 test "closure ledger copied owner and invalid phase reject atomically" {
1836 var storage: [test_storage_bytes]u8 align(Ledger.storage_alignment) =
1837 undefined;
1838 var ledger = try Ledger.init(&storage, test_limits);
1839 var fixture = Fixture.init();
1840 const initialization = storage;
1841 try std.testing.expectError(
1842 error.InvalidPhase,
1843 ledger.prepare(fixture.input(1, .{})),
1844 );
1845 try std.testing.expectError(error.InvalidPhase, ledger.publish());
1846 try std.testing.expectError(error.InvalidPhase, ledger.recover());
1847 try std.testing.expectError(error.InvalidPhase, ledger.snapshot());
1848 try std.testing.expectEqualSlices(u8, &initialization, &storage);
1849 ledger.activate();
1850 const no_stage = storage;
1851 try std.testing.expectError(error.NoStagedGeneration, ledger.publish());
1852 try std.testing.expectEqualSlices(u8, &no_stage, &storage);
1853 var copied = ledger;
1854 const activated = storage;
1855 try std.testing.expectError(
1856 error.CopiedOwner,
1857 copied.prepare(fixture.input(1, .{})),
1858 );
1859 try std.testing.expectError(error.CopiedOwner, copied.publish());
1860 try std.testing.expectError(error.CopiedOwner, copied.recover());
1861 try std.testing.expectError(error.CopiedOwner, copied.snapshot());
1862 try std.testing.expectEqualSlices(u8, &activated, &storage);
1863 _ = ledger.deinit();
1864 }
1865
1866 test "closure ledger abandons staged owner death exactly once" {
1867 comptime {
1868 @stardustClaim(
1869 @import("capacity").witness(Ledger, "closure_ledger_recovery"),
1870 null,
1871 null,
1872 null,
1873 null,
1874 null,
1875 null,
1876 );
1877 }
1878
1879 var storage: [test_storage_bytes]u8 align(Ledger.storage_alignment) =
1880 undefined;
1881 var ledger: Ledger = undefined;
1882 try initializeTestLedger(&ledger, &storage);
1883 var fixture = Fixture.init();
1884 try ledger.prepare(fixture.input(30, .{}));
1885 try ledger.publish();
1886 try ledger.prepare(fixture.input(31, .{}));
1887 try std.testing.expect(try ledger.recover());
1888 try std.testing.expectEqual(
1889 @as(schema.GenerationId, 30),
1890 (try ledger.snapshot()).?.id,
1891 );
1892 const recovered = storage;
1893 try std.testing.expect(!try ledger.recover());
1894 try std.testing.expectEqualSlices(u8, &recovered, &storage);
1895 try ledger.prepare(fixture.input(31, .{}));
1896 try ledger.publish();
1897 try std.testing.expectEqual(
1898 @as(schema.GenerationId, 31),
1899 (try ledger.snapshot()).?.id,
1900 );
1901 _ = ledger.deinit();
1902 }
1903
1904 test "closure ledger reports generation exhaustion before mutation" {
1905 var storage: [test_storage_bytes]u8 align(Ledger.storage_alignment) =
1906 undefined;
1907 var ledger: Ledger = undefined;
1908 try initializeTestLedger(&ledger, &storage);
1909 var fixture = Fixture.init();
1910 try ledger.prepare(
1911 fixture.input(std.math.maxInt(schema.GenerationId), .{}),
1912 );
1913 try ledger.publish();
1914 try expectAtomicPrepareError(
1915 error.GenerationExhausted,
1916 &ledger,
1917 fixture.input(1, .{}),
1918 &storage,
1919 );
1920 _ = ledger.deinit();
1921 }
1922
1923 test "closure ledger generation slots retain stable addresses" {
1924 var storage: [test_storage_bytes]u8 align(Ledger.storage_alignment) =
1925 undefined;
1926 var ledger: Ledger = undefined;
1927 try initializeTestLedger(&ledger, &storage);
1928 var fixture = Fixture.init();
1929 try ledger.prepare(fixture.input(1, .{}));
1930 try ledger.publish();
1931 const first = @intFromPtr((try ledger.snapshot()).?.nodes.ptr);
1932 try ledger.prepare(fixture.input(2, .{}));
1933 try ledger.publish();
1934 const second = @intFromPtr((try ledger.snapshot()).?.nodes.ptr);
1935 try std.testing.expect(first != second);
1936 try ledger.prepare(fixture.input(3, .{}));
1937 try ledger.publish();
1938 try std.testing.expectEqual(
1939 first,
1940 @intFromPtr((try ledger.snapshot()).?.nodes.ptr),
1941 );
1942 _ = ledger.deinit();
1943 }
1944
1945 test "closure ledger work remains within fixed transition limits" {
1946 comptime {
1947 @stardustClaim(
1948 @import("capacity").witness(Ledger, "closure_ledger_work"),
1949 null,
1950 null,
1951 null,
1952 null,
1953 null,
1954 null,
1955 );
1956 }
1957
1958 const maximum = try Ledger.Capacity.derive(.{});
1959 try std.testing.expect(
1960 maximum.storage_bytes * 2 <=
1961 Ledger.work_limits.transition_steps_max,
1962 );
1963 try std.testing.expectEqual(
1964 @as(usize, 1),
1965 Ledger.work_limits.cleanup_steps_per_call_max,
1966 );
1967 try std.testing.expectEqual(
1968 @as(usize, 1),
1969 Ledger.work_limits.cleanup_calls_at_capacity_max,
1970 );
1971 try std.testing.expect(
1972 !capacity.typeHasAllocatorCapability(Ledger),
1973 );
1974 }
1975
1976 comptime {
1977 std.debug.assert(@sizeOf(Header) <= limits.header_bytes);
1978 std.debug.assert(@alignOf(Header) <= limits.storage_alignment);
1979 std.debug.assert(@sizeOf(SlotHeader) <= limits.slot_header_bytes);
1980 std.debug.assert(@alignOf(SlotHeader) <= limits.storage_alignment);
1981 std.debug.assert(limits.nodes_max <= std.math.maxInt(u16));
1982 std.debug.assert(limits.edges_max <= std.math.maxInt(u16));
1983 std.debug.assert(limits.claim_nodes_max <= std.math.maxInt(u16));
1984 }
1985
1986 comptime {
1987 capacity.requireProvisionedRejectingOwnerShape(Ledger);
1988 }