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 }