lib/machine/src/world/canon.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const checkpoint = @import("../checkpoint/root.zig");
2 const fabric = @import("../fabric/root.zig");
3 const os = @import("os");
4 const std = @import("std");
5 const types = @import("types.zig");
6
7 const Sha256 = std.crypto.hash.sha2.Sha256;
8 const root_domain = "TINYMACHINEWORLDROOT1\x00";
9
10 /// The rejections of cut material. Each variant reports which check failed, whether
11 /// on a field's value, on the order of entries, or on a digest.
12 pub const Error = error{
13 ContractMismatch,
14 CutInvalid,
15 InvalidMachineRoot,
16 InvalidNode,
17 NodeCountMismatch,
18 NodesNotCanonical,
19 RootMismatch,
20 };
21
22 /// Builds a canonical cut from a ledger root together with nodes in strict order,
23 /// so a caller turns a ledger position and a node list into the one cut that stands
24 /// for them. The cut root digest covers every node together with the contract and
25 /// the ledger root. Node identities must ascend strictly and none may be all zero,
26 /// and a node count of zero or above the node limit rejects. Unused slots are filled
27 /// with the padding value.
28 pub fn prepare(
29 fabric_root: fabric.Root,
30 nodes: []const types.Node,
31 ) Error!types.Cut {
32 try validateMaterial(fabric_root, nodes);
33 var cut: types.Cut = .{
34 .root = undefined,
35 .nodes = @splat(emptyNode()),
36 };
37 @memcpy(cut.nodes[0..nodes.len], nodes);
38 cut.root = rootFor(fabric_root, nodes);
39 return cut;
40 }
41
42 /// Hashes a cut's material again and refuses the cut when the result differs from
43 /// the stored root. A node slot holding no node carries the empty padding value.
44 /// The cut's own contract must equal the contract its ledger root carries.
45 pub fn verify(cut: types.Cut) Error!void {
46 const count = cut.root.node_count;
47 if (count == 0 or count > types.node_limit) {
48 return error.NodeCountMismatch;
49 }
50 for (cut.nodes[count..]) |node| {
51 if (!std.meta.eql(node, emptyNode())) return error.CutInvalid;
52 }
53 const nodes = cut.nodes[0..count];
54 try validateMaterial(cut.root.fabric, nodes);
55 if (cut.root.dialect != .connected_world_v1 or
56 !std.meta.eql(
57 cut.root.machine_contract,
58 cut.root.fabric.machine_contract,
59 ))
60 {
61 return error.CutInvalid;
62 }
63 const expected = rootFor(cut.root.fabric, nodes);
64 if (!std.meta.eql(expected, cut.root)) return error.RootMismatch;
65 }
66
67 /// The padding value for unused cut slots. A caller uses this to fill the slots
68 /// of a cut it is building by hand. Every field of it is zero.
69 pub fn emptyNode() types.Node {
70 return .{
71 .id = .{ .bytes = @splat(0) },
72 .machine = .{
73 .digest = @splat(0),
74 .profile = .{ .digest = @splat(0) },
75 .state = .{ .digest = @splat(0) },
76 },
77 };
78 }
79
80 fn validateMaterial(
81 fabric_root: fabric.Root,
82 nodes: []const types.Node,
83 ) Error!void {
84 if (nodes.len == 0 or nodes.len > types.node_limit) {
85 return error.NodeCountMismatch;
86 }
87 try validateFabricRoot(fabric_root);
88 var previous: ?fabric.NodeId = null;
89 for (nodes) |node| {
90 if (os.abi.wire.allZero(&node.id.bytes)) return error.InvalidNode;
91 if (previous) |id| {
92 if (std.mem.order(u8, &id.bytes, &node.id.bytes) != .lt) {
93 return error.NodesNotCanonical;
94 }
95 }
96 try validateMachineRoot(node.machine);
97 previous = node.id;
98 }
99 }
100
101 fn validateFabricRoot(value: fabric.Root) Error!void {
102 os.abi.wire.validateDigest(value.digest) catch return error.CutInvalid;
103 os.abi.wire.validateDigest(value.machine_contract.digest) catch
104 return error.CutInvalid;
105 if (value.dialect != .ordered_effect_fabric_v3 or
106 value.entry_frontier < value.admission_frontier or
107 value.fault_frontier > value.admission_frontier)
108 {
109 return error.CutInvalid;
110 }
111 }
112
113 fn validateMachineRoot(value: checkpoint.Root) Error!void {
114 os.abi.wire.validateDigest(value.digest) catch
115 return error.InvalidMachineRoot;
116 os.abi.wire.validateDigest(value.profile.digest) catch
117 return error.InvalidMachineRoot;
118 os.abi.wire.validateDigest(value.state.digest) catch
119 return error.InvalidMachineRoot;
120 }
121
122 fn rootFor(fabric_root: fabric.Root, nodes: []const types.Node) types.Root {
123 var hasher = Sha256.init(.{});
124 hasher.update(root_domain);
125 hashInteger(&hasher, @backingInt(types.Dialect.connected_world_v1));
126 hasher.update(&fabric_root.machine_contract.digest);
127 hashFabricRoot(&hasher, fabric_root);
128 hashInteger(&hasher, @as(u8, @intCast(nodes.len)));
129 for (nodes) |node| {
130 hasher.update(&node.id.bytes);
131 hashMachineRoot(&hasher, node.machine);
132 }
133 var digest: os.abi.Digest = undefined;
134 hasher.final(&digest);
135 return .{
136 .digest = digest,
137 .dialect = .connected_world_v1,
138 .machine_contract = fabric_root.machine_contract,
139 .fabric = fabric_root,
140 .node_count = @intCast(nodes.len),
141 };
142 }
143
144 fn hashFabricRoot(hasher: *Sha256, value: fabric.Root) void {
145 hasher.update(&value.digest);
146 hashInteger(hasher, @backingInt(value.dialect));
147 hasher.update(&value.machine_contract.digest);
148 hashInteger(hasher, value.entry_frontier);
149 hashInteger(hasher, value.admission_frontier);
150 hashInteger(hasher, value.fault_frontier);
151 }
152
153 fn hashMachineRoot(hasher: *Sha256, value: checkpoint.Root) void {
154 hasher.update(&value.digest);
155 hasher.update(&value.profile.digest);
156 hasher.update(&value.state.digest);
157 }
158
159 fn hashInteger(hasher: *Sha256, value: anytype) void {
160 var encoded: [@sizeOf(@TypeOf(value))]u8 = undefined;
161 std.mem.writeInt(@TypeOf(value), &encoded, value, .little);
162 hasher.update(&encoded);
163 }