lib/machine/src/world/moment.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const fabric = @import("../fabric/root.zig");
2 const os = @import("os");
3 const std = @import("std");
4 const types = @import("types.zig");
5
6 const Sha256 = std.crypto.hash.sha2.Sha256;
7 const root_domain = "TINYMACHINEWORLDMOMENT1\x00";
8
9 /// The encoding version for moments. A reader checks this version before trusting
10 /// a stored moment, and rejects a moment written under a version it does not speak.
11 /// One version exists today: `stable_world_v1`.
12 pub const Dialect = enum(u16) {
13 stable_world_v1 = 1,
14 };
15
16 /// A place in one world's history, fixed by the origin cut it started from together
17 /// with the ledger root it stands at now. A caller compares moments to know whether
18 /// two worlds stand at the same place in the same history. Two moments match when
19 /// both the origin and the position match, and the digest covers the pair, so two
20 /// worlds that reached the same ledger position from different origins produce different
21 /// moments.
22 pub const Moment = struct {
23 digest: os.abi.Digest,
24 dialect: Dialect,
25 origin: types.Root,
26 fabric: fabric.Root,
27 };
28
29 pub const Error = error{
30 ContractMismatch,
31 FabricFrontierRegression,
32 InvalidFabricRoot,
33 InvalidWorldRoot,
34 MomentMismatch,
35 };
36
37 /// Produces the moment that belongs to a given origin world root and current ledger
38 /// root, so a caller records where the world now stands after every ledger change.
39 /// A contract mismatch between the origin and the current position rejects. Any
40 /// ledger counter that moved backward relative to the origin rejects: the entry,
41 /// admission, and fault frontiers are each checked.
42 pub fn prepare(origin: types.Root, current: fabric.Root) Error!Moment {
43 try validateMaterial(origin, current);
44 return rootFor(origin, current);
45 }
46
47 /// Derives the moment the material should yield and refuses a value that has drifted
48 /// from it, so a reader who holds a stored moment proves it belongs to this origin
49 /// and this position. The comparison covers every field, digest included.
50 pub fn verify(
51 value: Moment,
52 origin: types.Root,
53 current: fabric.Root,
54 ) Error!void {
55 const expected = try prepare(origin, current);
56 if (!std.meta.eql(value, expected)) return error.MomentMismatch;
57 }
58
59 fn validateMaterial(origin: types.Root, current: fabric.Root) Error!void {
60 try validateWorldRoot(origin);
61 try validateFabricRoot(current);
62 if (!std.meta.eql(origin.machine_contract, current.machine_contract)) {
63 return error.ContractMismatch;
64 }
65 if (current.entry_frontier < origin.fabric.entry_frontier or
66 current.admission_frontier < origin.fabric.admission_frontier or
67 current.fault_frontier < origin.fabric.fault_frontier)
68 {
69 return error.FabricFrontierRegression;
70 }
71 }
72
73 fn validateWorldRoot(value: types.Root) Error!void {
74 os.abi.wire.validateDigest(value.digest) catch
75 return error.InvalidWorldRoot;
76 os.abi.wire.validateDigest(value.machine_contract.digest) catch
77 return error.InvalidWorldRoot;
78 if (value.dialect != .connected_world_v1 or
79 value.node_count == 0 or
80 value.node_count > types.node_limit or
81 !std.meta.eql(value.machine_contract, value.fabric.machine_contract))
82 {
83 return error.InvalidWorldRoot;
84 }
85 validateFabricRoot(value.fabric) catch return error.InvalidWorldRoot;
86 }
87
88 fn validateFabricRoot(value: fabric.Root) Error!void {
89 os.abi.wire.validateDigest(value.digest) catch
90 return error.InvalidFabricRoot;
91 os.abi.wire.validateDigest(value.machine_contract.digest) catch
92 return error.InvalidFabricRoot;
93 if (value.dialect != .ordered_effect_fabric_v3 or
94 value.entry_frontier < value.admission_frontier or
95 value.fault_frontier > value.admission_frontier)
96 {
97 return error.InvalidFabricRoot;
98 }
99 }
100
101 fn rootFor(origin: types.Root, current: fabric.Root) Moment {
102 var hasher = Sha256.init(.{});
103 hasher.update(root_domain);
104 hashInteger(&hasher, @backingInt(Dialect.stable_world_v1));
105 hashWorldRoot(&hasher, origin);
106 hashFabricRoot(&hasher, current);
107 var digest: os.abi.Digest = undefined;
108 hasher.final(&digest);
109 return .{
110 .digest = digest,
111 .dialect = .stable_world_v1,
112 .origin = origin,
113 .fabric = current,
114 };
115 }
116
117 fn hashWorldRoot(hasher: *Sha256, value: types.Root) void {
118 hasher.update(&value.digest);
119 hashInteger(hasher, @backingInt(value.dialect));
120 hasher.update(&value.machine_contract.digest);
121 hashFabricRoot(hasher, value.fabric);
122 hashInteger(hasher, value.node_count);
123 }
124
125 fn hashFabricRoot(hasher: *Sha256, value: fabric.Root) void {
126 hasher.update(&value.digest);
127 hashInteger(hasher, @backingInt(value.dialect));
128 hasher.update(&value.machine_contract.digest);
129 hashInteger(hasher, value.entry_frontier);
130 hashInteger(hasher, value.admission_frontier);
131 hashInteger(hasher, value.fault_frontier);
132 }
133
134 fn hashInteger(hasher: *Sha256, value: anytype) void {
135 var encoded: [@sizeOf(@TypeOf(value))]u8 = undefined;
136 std.mem.writeInt(@TypeOf(value), &encoded, value, .little);
137 hasher.update(&encoded);
138 }
139
140 test "moments separate equal fabric positions by exact origin" {
141 const contract = @import("../profile/root.zig").ContractFingerprint{
142 .digest = @splat(0x31),
143 };
144 const initial: fabric.Root = .{
145 .digest = @splat(0x41),
146 .dialect = .ordered_effect_fabric_v3,
147 .machine_contract = contract,
148 .entry_frontier = 4,
149 .admission_frontier = 2,
150 .fault_frontier = 1,
151 };
152 const current: fabric.Root = .{
153 .digest = @splat(0x42),
154 .dialect = .ordered_effect_fabric_v3,
155 .machine_contract = contract,
156 .entry_frontier = 6,
157 .admission_frontier = 3,
158 .fault_frontier = 2,
159 };
160 const first_origin: types.Root = .{
161 .digest = @splat(0x51),
162 .dialect = .connected_world_v1,
163 .machine_contract = contract,
164 .fabric = initial,
165 .node_count = 2,
166 };
167 var second_origin = first_origin;
168 second_origin.digest = @splat(0x52);
169 const first = try prepare(first_origin, current);
170 const second = try prepare(second_origin, current);
171 try std.testing.expect(!std.meta.eql(first, second));
172 try verify(first, first_origin, current);
173 try std.testing.expectError(
174 error.MomentMismatch,
175 verify(first, second_origin, current),
176 );
177 }