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 }