lib/machine/src/admission/canon.zig
daab053ee43316e1809a84551d573ddd1e5bf3d2
1 const os = @import("os");
2 const std = @import("std");
3 const types = @import("types.zig");
4
5 const Sha256 = std.crypto.hash.sha2.Sha256;
6 const domain = "TINYMACHINEADMISSION1\x00";
7 const delivery_domain = "TINYMACHINEDELIVERY1\x00";
8
9 /// A caller matches on these rejections of admission material to tell a malformed
10 /// input apart from one that arrived out of order. Every variant means one of four
11 /// things failed: validation of a field, a payload bound, the ordering against a
12 /// frontier, or the recomputation of a digest.
13 pub const Error = error{
14 ContractDigestInvalid,
15 EffectFrontierExhausted,
16 EffectRequestInvalid,
17 EffectRequestPending,
18 EffectReceiptInvalid,
19 EffectOutputRootInvalid,
20 EntropyGenerationExhausted,
21 EntropyGenerationMismatch,
22 InputFrontierExhausted,
23 DeliveryFenceInvalid,
24 DeliveryFenceMismatch,
25 NoncanonicalDelivery,
26 NoncanonicalRecord,
27 PayloadCapacityExceeded,
28 PayloadEmpty,
29 SourceRootInvalid,
30 TerminalOffsetExhausted,
31 TerminalOffsetMismatch,
32 VirtualTimeMismatch,
33 };
34
35 /// Builds a terminal record at one offset from the caller's bytes for each chunk
36 /// of input bytes the caller wants the guest to read, copying them into the record's
37 /// fixed storage. An empty payload is rejected, and so is one longer than the terminal
38 /// bound.
39 pub fn terminal(offset: u64, bytes: []const u8) Error!types.Record {
40 if (bytes.len == 0) return error.PayloadEmpty;
41 if (bytes.len > types.terminal_bytes_max) return error.PayloadCapacityExceeded;
42 var value: types.Terminal = .{
43 .offset = offset,
44 .length = @intCast(bytes.len),
45 .storage = @splat(0),
46 };
47 @memcpy(value.storage[0..bytes.len], bytes);
48 return .{ .terminal = value };
49 }
50
51 /// Builds a clock advance from one tick to another so a caller can move the guest's
52 /// clock. The clock advance is admitted only when the tick it moves to is strictly
53 /// greater than the tick it leaves.
54 pub fn virtualTime(from_tick: u64, to_tick: u64) Error!types.Record {
55 if (to_tick <= from_tick) return error.VirtualTimeMismatch;
56 return .{ .virtual_time = .{
57 .from_tick = from_tick,
58 .to_tick = to_tick,
59 } };
60 }
61
62 /// Builds an entropy record for one generation from the caller's bytes so a caller
63 /// can hand the guest a recorded block of randomness, copying them into the record's
64 /// fixed storage. An empty payload is rejected, and so is one longer than the entropy
65 /// bound.
66 pub fn entropy(generation: u64, bytes: []const u8) Error!types.Record {
67 if (bytes.len == 0) return error.PayloadEmpty;
68 if (bytes.len > types.entropy_bytes_max) return error.PayloadCapacityExceeded;
69 var value: types.Entropy = .{
70 .generation = generation,
71 .length = @intCast(bytes.len),
72 .storage = @splat(0),
73 };
74 @memcpy(value.storage[0..bytes.len], bytes);
75 return .{ .entropy = value };
76 }
77
78 /// Copies the caller's bytes into fixed storage for a caller answering the outstanding
79 /// guest request, pairing them with the receipt and correlation of the request being
80 /// answered, the status of the effect, and the root of its output. A payload longer
81 /// than the effect-result bound is rejected. By contrast, an empty payload is accepted,
82 /// so a result that reports a status and carries no bytes passes.
83 pub fn effectResult(
84 request_receipt: types.Digest,
85 correlation: u64,
86 status: os.abi.EffectStatus,
87 output_root: types.Digest,
88 bytes: []const u8,
89 ) Error!types.Record {
90 if (bytes.len > types.effect_result_bytes_max) {
91 return error.PayloadCapacityExceeded;
92 }
93 var value: types.EffectResult = .{
94 .request = .{
95 .receipt = .{ .digest = request_receipt },
96 .correlation = correlation,
97 },
98 .status = status,
99 .output_root = output_root,
100 .length = @intCast(bytes.len),
101 .storage = @splat(0),
102 };
103 @memcpy(value.storage[0..bytes.len], bytes);
104 return .{ .effect_result = value };
105 }
106
107 /// Applies one record to a basis it has validated, then hashes the outcome into
108 /// the receipt it returns, so a caller can deliver the resulting admission and a
109 /// verifier can recheck it. The input position is the input frontier plus one. While
110 /// an effect request is outstanding, only its result admits. Frontier arithmetic
111 /// is checked for overflow, so a counter at its limit rejects the record and holds
112 /// its value.
113 pub fn prepare(basis: types.Basis, record: types.Record) Error!types.Admission {
114 try validateBasis(basis);
115 const position = std.math.add(u64, basis.frontiers.input, 1) catch
116 return error.InputFrontierExhausted;
117 var expected = basis.frontiers;
118 var expected_outstanding_effect = basis.outstanding_effect;
119 expected.input = position;
120 try fold(&expected, &expected_outstanding_effect, record);
121 var result: types.Admission = .{
122 .contract = basis.contract,
123 .source_root = basis.source_root,
124 .position = position,
125 .frontiers = basis.frontiers,
126 .record = record,
127 .expected = expected,
128 .outstanding_effect = basis.outstanding_effect,
129 .expected_outstanding_effect = expected_outstanding_effect,
130 .receipt = .{ .digest = undefined },
131 };
132 result.receipt.digest = digest(&result);
133 return result;
134 }
135
136 /// Rejects a basis whose contract digest, source root, or outstanding effect request
137 /// fails validation, so a caller can check the basis before building a record against
138 /// it. An all-zero contract digest and an all-zero source root are both rejected.
139 /// An outstanding request whose correlation is other than the effect frontier plus
140 /// one is rejected.
141 pub fn validateBasis(basis: types.Basis) Error!void {
142 try validateDigest(basis.contract.digest, error.ContractDigestInvalid);
143 try validateDigest(basis.source_root, error.SourceRootInvalid);
144 try validateEffectRequest(basis);
145 }
146
147 /// The call recomputes the expected admission from the basis and the record, then
148 /// rejects a value that differs anywhere, so a reader holding a recorded admission
149 /// can prove it unchanged. The comparison covers every field, receipt included,
150 /// so a changed payload byte fails it.
151 pub fn verify(basis: types.Basis, value: *const types.Admission) Error!void {
152 const expected = try prepare(basis, value.record);
153 if (!std.meta.eql(expected, value.*)) return error.NoncanonicalRecord;
154 }
155
156 /// Joins one admission with a fence it has validated and with the source root that
157 /// follows, then hashes the whole into a delivery receipt, so a caller can produce
158 /// the delivery form the guest accepts. An all-zero next source root is rejected,
159 /// and so is a fence with a zero generation, an all-zero world, or an all-zero token.
160 pub fn bindDelivery(
161 value: types.Admission,
162 next_source_root: types.Digest,
163 fence: os.abi.ActivationFence,
164 ) Error!types.Delivery {
165 try validateDigest(next_source_root, error.SourceRootInvalid);
166 os.abi.wire.validateFence(fence) catch
167 return error.DeliveryFenceInvalid;
168 var result: types.Delivery = .{
169 .admission = value,
170 .next_source_root = next_source_root,
171 .fence = fence,
172 .receipt = .{ .digest = undefined },
173 };
174 result.receipt.digest = deliveryDigest(result);
175 return result;
176 }
177
178 /// Checks a delivery in three layers, innermost first, so a reader holding a recorded
179 /// delivery can prove both the delivery and the admission inside it: its fence has
180 /// to equal the one the caller names, the admission it wraps has to verify against
181 /// the basis, and its own receipt has to hash to the stored value.
182 pub fn verifyDelivery(
183 basis: types.Basis,
184 fence: os.abi.ActivationFence,
185 value: *const types.Delivery,
186 ) Error!void {
187 try validateDigest(value.next_source_root, error.SourceRootInvalid);
188 os.abi.wire.validateFence(value.fence) catch
189 return error.DeliveryFenceInvalid;
190 if (!os.abi.wire.equalFence(fence, value.fence)) {
191 return error.DeliveryFenceMismatch;
192 }
193 try verify(basis, &value.admission);
194 const expected = try bindDelivery(
195 value.admission,
196 value.next_source_root,
197 value.fence,
198 );
199 if (!std.meta.eql(expected, value.*)) return error.NoncanonicalDelivery;
200 }
201
202 fn deliveryDigest(value: types.Delivery) types.Digest {
203 var hasher = Sha256.init(.{});
204 hasher.update(delivery_domain);
205 hasher.update(&value.admission.receipt.digest);
206 hasher.update(&value.next_source_root);
207 hasher.update(&value.fence.world);
208 hashInteger(&hasher, value.fence.generation);
209 hasher.update(&value.fence.token);
210 var output: types.Digest = undefined;
211 hasher.final(&output);
212 return output;
213 }
214
215 fn fold(
216 expected: *types.Frontiers,
217 expected_outstanding_effect: *?types.EffectRequest,
218 record: types.Record,
219 ) Error!void {
220 if (expected_outstanding_effect.* != null and
221 std.meta.activeTag(record) != .effect_result)
222 {
223 return error.EffectRequestPending;
224 }
225 switch (record) {
226 .terminal => |value| {
227 try validateLength(value.length, value.storage.len);
228 try validateTail(value.storage[value.length..]);
229 if (value.length == 0) return error.PayloadEmpty;
230 if (value.offset != expected.terminal_input_offset) {
231 return error.TerminalOffsetMismatch;
232 }
233 expected.terminal_input_offset = std.math.add(
234 u64,
235 value.offset,
236 value.length,
237 ) catch return error.TerminalOffsetExhausted;
238 },
239 .virtual_time => |value| {
240 if (value.from_tick != expected.virtual_time_tick or
241 value.to_tick <= value.from_tick)
242 {
243 return error.VirtualTimeMismatch;
244 }
245 expected.virtual_time_tick = value.to_tick;
246 },
247 .entropy => |value| {
248 try validateLength(value.length, value.storage.len);
249 try validateTail(value.storage[value.length..]);
250 if (value.length == 0) return error.PayloadEmpty;
251 const generation = std.math.add(
252 u64,
253 expected.entropy_generation,
254 1,
255 ) catch return error.EntropyGenerationExhausted;
256 if (value.generation != generation) {
257 return error.EntropyGenerationMismatch;
258 }
259 expected.entropy_generation = generation;
260 },
261 .effect_result => |value| {
262 try validateLength(value.length, value.storage.len);
263 try validateTail(value.storage[value.length..]);
264 try validateDigest(
265 value.request.receipt.digest,
266 error.EffectReceiptInvalid,
267 );
268 try validateDigest(value.output_root, error.EffectOutputRootInvalid);
269 const request = expected_outstanding_effect.* orelse
270 return error.EffectRequestInvalid;
271 if (!std.meta.eql(value.request, request)) {
272 return error.EffectReceiptInvalid;
273 }
274 expected.effect = request.correlation;
275 expected_outstanding_effect.* = null;
276 },
277 }
278 }
279
280 fn digest(value: *const types.Admission) types.Digest {
281 var hasher = Sha256.init(.{});
282 hasher.update(domain);
283 hasher.update(&value.contract.digest);
284 hasher.update(&value.source_root);
285 hashInteger(&hasher, value.position);
286 hashFrontiers(&hasher, value.frontiers);
287 hashEffectRequest(&hasher, value.outstanding_effect);
288 switch (value.record) {
289 .terminal => |record| {
290 hashInteger(&hasher, @as(u8, 1));
291 hashInteger(&hasher, record.offset);
292 hashBytes(&hasher, record.storage[0..record.length]);
293 },
294 .virtual_time => |record| {
295 hashInteger(&hasher, @as(u8, 2));
296 hashInteger(&hasher, record.from_tick);
297 hashInteger(&hasher, record.to_tick);
298 },
299 .entropy => |record| {
300 hashInteger(&hasher, @as(u8, 3));
301 hashInteger(&hasher, record.generation);
302 hashBytes(&hasher, record.storage[0..record.length]);
303 },
304 .effect_result => |record| {
305 hashInteger(&hasher, @as(u8, 4));
306 hasher.update(&record.request.receipt.digest);
307 hashInteger(&hasher, record.request.correlation);
308 hashInteger(&hasher, @backingInt(record.status));
309 hasher.update(&record.output_root);
310 hashBytes(&hasher, record.storage[0..record.length]);
311 },
312 }
313 var output: types.Digest = undefined;
314 hasher.final(&output);
315 return output;
316 }
317
318 fn hashEffectRequest(hasher: *Sha256, value: ?types.EffectRequest) void {
319 const request = value orelse {
320 hashInteger(hasher, @as(u8, 0));
321 return;
322 };
323 hashInteger(hasher, @as(u8, 1));
324 hasher.update(&request.receipt.digest);
325 hashInteger(hasher, request.correlation);
326 }
327
328 fn hashFrontiers(hasher: *Sha256, value: types.Frontiers) void {
329 hashInteger(hasher, value.input);
330 hashInteger(hasher, value.terminal_input_offset);
331 hashInteger(hasher, value.virtual_time_tick);
332 hashInteger(hasher, value.entropy_generation);
333 hashInteger(hasher, value.effect);
334 }
335
336 fn hashBytes(hasher: *Sha256, bytes: []const u8) void {
337 hashInteger(hasher, @as(u16, @intCast(bytes.len)));
338 hasher.update(bytes);
339 }
340
341 fn hashInteger(hasher: *Sha256, value: anytype) void {
342 var encoded: [@sizeOf(@TypeOf(value))]u8 = undefined;
343 std.mem.writeInt(@TypeOf(value), &encoded, value, .little);
344 hasher.update(&encoded);
345 }
346
347 fn validateDigest(value: types.Digest, failure: Error) Error!void {
348 os.abi.wire.validateDigest(value) catch return failure;
349 }
350
351 fn validateEffectRequest(basis: types.Basis) Error!void {
352 const request = basis.outstanding_effect orelse return;
353 try validateDigest(request.receipt.digest, error.EffectRequestInvalid);
354 const expected = std.math.add(u64, basis.frontiers.effect, 1) catch
355 return error.EffectFrontierExhausted;
356 if (request.correlation != expected) return error.EffectRequestInvalid;
357 }
358
359 fn validateLength(length: u16, capacity: usize) Error!void {
360 if (length > capacity) return error.PayloadCapacityExceeded;
361 }
362
363 fn validateTail(bytes: []const u8) Error!void {
364 if (!os.abi.wire.allZero(bytes)) return error.NoncanonicalRecord;
365 }
366
367 comptime {
368 std.debug.assert(types.terminal_bytes_max <= std.math.maxInt(u16));
369 std.debug.assert(types.entropy_bytes_max <= std.math.maxInt(u16));
370 std.debug.assert(types.effect_result_bytes_max <= std.math.maxInt(u16));
371 }