lib/peer/src/ticket.zig

daab053ee43316e1809a84551d573ddd1e5bf3d2

  1 const std = @import("std");
  2 const alloc_phase = @import("alloc_phase");
  3 const address_mod = @import("address.zig");
  4 const identity = @import("identity.zig");
  5 const text = @import("text.zig");
  6 
  7 const Address = address_mod.Address;
  8 const PeerId = identity.PeerId;
  9 
 10 pub const magic = "TNYP".*;
 11 pub const version: u8 = 1;
 12 pub const text_prefix = "tiny-peer:";
 13 /// The largest number of addresses one ticket can carry. Version 1 of the
 14 /// ticket format writes the address count in one octet and reads it back from
 15 /// one octet, which sets the ceiling at 255.
 16 pub const address_count_max: usize = std.math.maxInt(u8);
 17 pub const wire_header_bytes: usize = magic.len + 1 + identity.key_bytes + 1;
 18 pub const address_wire_bytes_max: usize = 1 + 1 + address_mod.relay_host_bytes_max + 2;
 19 
 20 const Tag = enum(u8) {
 21     ip4 = 1,
 22     ip6 = 2,
 23     relay = 3,
 24 };
 25 
 26 pub const Limits = struct {
 27     max_addresses: u8,
 28 };
 29 
 30 pub const CapacityDeriveError = error{
 31     CapacityOverflow,
 32 };
 33 
 34 pub const Capacity = struct {
 35     max_addresses: u8,
 36     wire_bytes: u17,
 37     text_bytes: u17,
 38     storage_bytes: usize,
 39 
 40     pub const DeriveError: type = CapacityDeriveError;
 41 
 42     pub fn derive(limits: Limits) DeriveError!Capacity {
 43         const bodies = std.math.mul(
 44             usize,
 45             limits.max_addresses,
 46             address_wire_bytes_max,
 47         ) catch return error.CapacityOverflow;
 48         const wire_bytes = std.math.add(usize, wire_header_bytes, bodies) catch
 49             return error.CapacityOverflow;
 50         const text_body = text.encodedLength(wire_bytes);
 51         const text_bytes = std.math.add(usize, text_prefix.len, text_body) catch
 52             return error.CapacityOverflow;
 53         const storage_bytes = std.math.mul(
 54             usize,
 55             limits.max_addresses,
 56             @sizeOf(Address),
 57         ) catch return error.CapacityOverflow;
 58         return .{
 59             .max_addresses = limits.max_addresses,
 60             .wire_bytes = std.math.cast(u17, wire_bytes) orelse
 61                 return error.CapacityOverflow,
 62             .text_bytes = std.math.cast(u17, text_bytes) orelse
 63                 return error.CapacityOverflow,
 64             .storage_bytes = storage_bytes,
 65         };
 66     }
 67 };
 68 
 69 const TicketLimits = Limits;
 70 const TicketCapacity = Capacity;
 71 
 72 pub const TicketStorage = struct {
 73     pub const storage_alignment: usize = @alignOf(Address);
 74     pub const Storage = []align(storage_alignment) u8;
 75     pub const Limits: type = TicketLimits;
 76     pub const Capacity: type = TicketCapacity;
 77     pub const Exhaustion = error{TooManyAddresses};
 78     pub const InitError = TicketCapacity.DeriveError || error{StorageTooShort};
 79     pub const work_limits: alloc_phase.capacity.WorkLimits = .{
 80         .transition_steps_max = 1,
 81         .cleanup_steps_per_call_max = 0,
 82         .cleanup_calls_at_capacity_max = 0,
 83     };
 84 
 85     pub const claim: alloc_phase.capacity.Declaration = .{
 86         .source = .{
 87             .id = "peer.bootstrap_ticket",
 88             .kind = .phase_static,
 89             .limit_source = .caller,
 90             .storage = .{
 91                 .covered = &.{.{
 92                     .id = "caller_bootstrap_address_slots",
 93                     .lifetime = .transferred,
 94                     .detail = "caller address slots including inline relay host bytes",
 95                 }},
 96                 .excluded = &.{
 97                     "fixed peer identifier, count, capacity, and phase fields",
 98                     "caller-owned wire, text, and codec scratch buffers",
 99                 },
100             },
101             .capacity = .{
102                 .inputs = &.{
103                     alloc_phase.capacity.bindInput(
104                         TicketLimits,
105                         "max_addresses",
106                         "max_addresses",
107                     ),
108                 },
109                 .type_selectors = &.{
110                     alloc_phase.capacity.bindType(Address, "address"),
111                 },
112                 .nodes = &.{
113                     .{ .input = 0 },
114                     .{ .scale = .{
115                         .node = 0,
116                         .coefficient = .{ .size_of_concrete_type = 0 },
117                     } },
118                 },
119                 .assertions = &.{.{
120                     .scope = .closure_total,
121                     .measure = .retained,
122                     .relation = .exact,
123                     .expression = 1,
124                 }},
125             },
126             .overload = .{
127                 .kind = .reject_before_mutation,
128                 .detail = "max-plus-one address admission rejects before storage mutation",
129             },
130             .risks = .{
131                 .transitive = .{
132                     .status = .witnessed,
133                     .detail = "ticket codecs and admission use caller slices and value operations",
134                 },
135                 .foreign = .{
136                     .status = .excluded,
137                     .detail = "ticket storage crosses no operating-system or foreign boundary",
138                 },
139             },
140             .work = .{
141                 .equation = "activate <= 1 and codec steps <= max_addresses",
142             },
143             .obligations = &.{
144                 .{ .key = "peer_ticket_capacity", .role = .capacity_model },
145                 .{ .key = "peer_ticket_overload", .role = .overload },
146                 .{ .key = "peer_ticket_transitive", .role = .transitive_risk },
147                 .{ .key = "peer_ticket_work", .role = .work_bound },
148             },
149         },
150         .bindings = .{
151             .owner = @This(),
152             .seal = .{
153                 .family = alloc_phase.capacity.selector(@This().activate),
154                 .premise = .{
155                     .class = .checked_semantic_fact,
156                     .authority = .checker,
157                 },
158             },
159             .teardown = .{
160                 .family = alloc_phase.capacity.selector(@This().deinit),
161                 .premise = .{
162                     .class = .checked_semantic_fact,
163                     .authority = .checker,
164                 },
165             },
166         },
167     };
168 
169     phase: alloc_phase.capacity.Phase,
170     capacity: TicketCapacity,
171     storage: Storage,
172     address_count: u8,
173 
174     pub fn init(bytes: Storage, limits: TicketLimits) InitError!TicketStorage {
175         const capacity = try TicketCapacity.derive(limits);
176         if (bytes.len < capacity.storage_bytes) return error.StorageTooShort;
177         return .{
178             .phase = .initialization,
179             .capacity = capacity,
180             .storage = bytes[0..capacity.storage_bytes],
181             .address_count = 0,
182         };
183     }
184 
185     pub fn activate(self: *TicketStorage) void {
186         std.debug.assert(self.phase == .initialization);
187         self.phase = .steady;
188     }
189 
190     pub fn append(self: *TicketStorage, address: Address) Exhaustion!void {
191         self.assertSteady();
192         if (self.address_count >= self.capacity.max_addresses) {
193             return error.TooManyAddresses;
194         }
195         self.slots()[self.address_count] = address;
196         self.address_count += 1;
197     }
198 
199     pub fn items(self: *const TicketStorage) []const Address {
200         self.assertSteady();
201         const pointer: [*]const Address = @ptrCast(self.storage.ptr);
202         return pointer[0..self.address_count];
203     }
204 
205     pub fn deinit(self: *TicketStorage) Storage {
206         std.debug.assert(self.phase == .steady);
207         self.phase = .teardown;
208         const bytes = self.storage;
209         self.* = undefined;
210         return bytes;
211     }
212 
213     fn slots(self: *TicketStorage) []Address {
214         self.assertSteady();
215         const pointer: [*]Address = @ptrCast(self.storage.ptr);
216         return pointer[0..self.capacity.max_addresses];
217     }
218 
219     fn assertSteady(self: *const TicketStorage) void {
220         std.debug.assert(self.phase == .steady);
221         std.debug.assert(self.address_count <= self.capacity.max_addresses);
222         std.debug.assert(self.storage.len == self.capacity.storage_bytes);
223     }
224 };
225 
226 comptime {
227     alloc_phase.capacity.requireProvisionedRejectingOwnerShape(TicketStorage);
228 }
229 
230 pub const Ticket = struct {
231     peer: PeerId,
232     address_storage: TicketStorage,
233 
234     pub const storage_alignment: usize = TicketStorage.storage_alignment;
235     pub const Storage: type = TicketStorage.Storage;
236     pub const Limits: type = TicketLimits;
237     pub const Capacity: type = TicketCapacity;
238     pub const InitError: type = TicketStorage.InitError;
239     pub const AppendError: type = TicketStorage.Exhaustion;
240 
241     pub fn init(peer: PeerId, storage: Storage, limits: TicketLimits) InitError!Ticket {
242         var address_storage = try TicketStorage.init(storage, limits);
243         address_storage.activate();
244         return .{ .peer = peer, .address_storage = address_storage };
245     }
246 
247     pub fn deinit(self: *Ticket) Storage {
248         const storage = self.address_storage.deinit();
249         self.* = undefined;
250         return storage;
251     }
252 
253     pub fn append(self: *Ticket, address: Address) AppendError!void {
254         return self.address_storage.append(address);
255     }
256 
257     pub fn items(self: *const Ticket) []const Address {
258         return self.address_storage.items();
259     }
260 
261     pub fn eql(self: *const Ticket, other: *const Ticket) bool {
262         if (!self.peer.eql(other.peer)) return false;
263         if (self.items().len != other.items().len) return false;
264         for (self.items(), other.items()) |left, right| {
265             if (!left.eql(right)) return false;
266         }
267         return true;
268     }
269 
270     pub fn formatText(
271         self: *const Ticket,
272         wire: []u8,
273         output: []u8,
274     ) TextEncodeError!usize {
275         const wire_len = try encode(self, wire);
276         const encoded_len = text.encodedLength(wire_len);
277         const needed = text_prefix.len + encoded_len;
278         if (output.len < needed) return error.OutputTooSmall;
279         @memcpy(output[0..text_prefix.len], text_prefix);
280         _ = try text.encode(
281             wire[0..wire_len],
282             output[text_prefix.len..needed],
283         );
284         return needed;
285     }
286 
287     pub fn parseText(
288         source: []const u8,
289         wire: []u8,
290         storage: Storage,
291         limits: TicketLimits,
292     ) TextDecodeError!Ticket {
293         return parseTextValue(source, wire, storage, limits);
294     }
295 };
296 
297 pub const EncodeError = error{
298     InvalidAddress,
299     InvalidTicket,
300     OutputTooSmall,
301 };
302 
303 pub const DecodeError = error{
304     InvalidAddress,
305     InvalidLimits,
306     StorageTooShort,
307     TooManyAddresses,
308     TrailingBytes,
309     Truncated,
310     UnknownAddressKind,
311     UnknownVersion,
312     WrongMagic,
313 };
314 
315 pub const TextParseError = error{
316     InvalidText,
317     MissingPrefix,
318     TicketTooLong,
319 };
320 
321 pub const TextEncodeError = EncodeError || text.EncodeError;
322 pub const TextDecodeError = DecodeError || TextParseError;
323 
324 pub fn encode(ticket: *const Ticket, output: []u8) EncodeError!usize {
325     const needed = try encodedWireLength(ticket);
326     if (output.len < needed) return error.OutputTooSmall;
327     var writer = WireWriter{ .output = output[0..needed] };
328     writer.putSlice(&magic);
329     writer.putByte(version);
330     writer.putSlice(&ticket.peer.key);
331     writer.putByte(@intCast(ticket.items().len));
332     for (ticket.items()) |address| {
333         writeAddress(&writer, address);
334     }
335     std.debug.assert(writer.index == needed);
336     return writer.index;
337 }
338 
339 pub fn decode(
340     source: []const u8,
341     storage: Ticket.Storage,
342     limits: Limits,
343 ) DecodeError!Ticket {
344     var address_storage = TicketStorage.init(storage, limits) catch |err| switch (err) {
345         error.CapacityOverflow => return error.InvalidLimits,
346         error.StorageTooShort => return error.StorageTooShort,
347     };
348     address_storage.activate();
349     errdefer _ = address_storage.deinit();
350     var cursor = Cursor{ .source = source };
351     const actual_magic = try cursor.take(magic.len);
352     if (!std.mem.eql(u8, actual_magic, &magic)) return error.WrongMagic;
353     if (try cursor.byte() != version) return error.UnknownVersion;
354     const peer = PeerId{ .key = (try cursor.take(identity.key_bytes))[0..identity.key_bytes].* };
355     const count = try cursor.byte();
356     if (count > address_storage.capacity.max_addresses) return error.TooManyAddresses;
357     var ticket = Ticket{
358         .peer = peer,
359         .address_storage = address_storage,
360     };
361     for (0..address_count_max) |index| {
362         if (index >= count) break;
363         ticket.append(try decodeAddress(&cursor)) catch return error.TooManyAddresses;
364     }
365     if (cursor.index != source.len) return error.TrailingBytes;
366     return ticket;
367 }
368 
369 fn parseTextValue(
370     source: []const u8,
371     wire: []u8,
372     storage: Ticket.Storage,
373     limits: Limits,
374 ) TextDecodeError!Ticket {
375     if (!std.mem.startsWith(u8, source, text_prefix)) return error.MissingPrefix;
376     const encoded = source[text_prefix.len..];
377     const wire_len = text.decode(encoded, wire) catch |err| switch (err) {
378         error.OutputTooSmall => return error.TicketTooLong,
379         error.InvalidEncoding, error.InvalidLength => return error.InvalidText,
380     };
381     return decode(wire[0..wire_len], storage, limits);
382 }
383 
384 fn encodedWireLength(ticket: *const Ticket) EncodeError!usize {
385     if (ticket.items().len > ticket.address_storage.capacity.max_addresses) {
386         return error.InvalidTicket;
387     }
388     var result = wire_header_bytes;
389     for (ticket.items()) |address| {
390         const address_len: usize = switch (address) {
391             .ip4 => 1 + 4 + 2,
392             .ip6 => 1 + 16 + 2,
393             .relay => |relay| blk: {
394                 if (!relay.valid()) return error.InvalidAddress;
395                 break :blk 1 + 1 + @as(usize, relay.len) + 2;
396             },
397         };
398         result = std.math.add(usize, result, address_len) catch
399             return error.InvalidTicket;
400     }
401     return result;
402 }
403 
404 fn writeAddress(writer: *WireWriter, address: Address) void {
405     switch (address) {
406         .ip4 => |value| {
407             writer.putByte(@backingInt(Tag.ip4));
408             writer.putSlice(&value.addr);
409             writer.putU16(value.port);
410         },
411         .ip6 => |value| {
412             writer.putByte(@backingInt(Tag.ip6));
413             writer.putSlice(&value.addr);
414             writer.putU16(value.port);
415         },
416         .relay => |value| {
417             std.debug.assert(value.valid());
418             writer.putByte(@backingInt(Tag.relay));
419             writer.putByte(value.len);
420             writer.putSlice(value.host());
421             writer.putU16(value.port);
422         },
423     }
424 }
425 
426 fn decodeAddress(cursor: *Cursor) DecodeError!Address {
427     return switch (try cursor.byte()) {
428         @backingInt(Tag.ip4) => .{ .ip4 = .{
429             .addr = (try cursor.take(4))[0..4].*,
430             .port = try cursor.u16Big(),
431         } },
432         @backingInt(Tag.ip6) => .{ .ip6 = .{
433             .addr = (try cursor.take(16))[0..16].*,
434             .port = try cursor.u16Big(),
435         } },
436         @backingInt(Tag.relay) => blk: {
437             const length = try cursor.byte();
438             if (length == 0 or length > address_mod.relay_host_bytes_max) {
439                 return error.InvalidAddress;
440             }
441             const host_name = try cursor.take(length);
442             const port = try cursor.u16Big();
443             break :blk Address.fromRelay(host_name, port) catch
444                 return error.InvalidAddress;
445         },
446         else => error.UnknownAddressKind,
447     };
448 }
449 
450 const WireWriter = struct {
451     output: []u8,
452     index: usize = 0,
453 
454     fn putByte(self: *WireWriter, value: u8) void {
455         std.debug.assert(self.index < self.output.len);
456         self.output[self.index] = value;
457         self.index += 1;
458     }
459 
460     fn putSlice(self: *WireWriter, value: []const u8) void {
461         std.debug.assert(self.index <= self.output.len);
462         std.debug.assert(value.len <= self.output.len - self.index);
463         @memcpy(self.output[self.index..][0..value.len], value);
464         self.index += value.len;
465     }
466 
467     fn putU16(self: *WireWriter, value: u16) void {
468         std.debug.assert(self.index <= self.output.len);
469         std.debug.assert(2 <= self.output.len - self.index);
470         std.mem.writeInt(u16, self.output[self.index..][0..2], value, .big);
471         self.index += 2;
472     }
473 };
474 
475 const Cursor = struct {
476     source: []const u8,
477     index: usize = 0,
478 
479     fn take(self: *Cursor, length: usize) DecodeError![]const u8 {
480         if (self.index > self.source.len) return error.Truncated;
481         if (length > self.source.len - self.index) return error.Truncated;
482         const result = self.source[self.index..][0..length];
483         self.index += length;
484         return result;
485     }
486 
487     fn byte(self: *Cursor) DecodeError!u8 {
488         return (try self.take(1))[0];
489     }
490 
491     fn u16Big(self: *Cursor) DecodeError!u16 {
492         return std.mem.readInt(u16, (try self.take(2))[0..2], .big);
493     }
494 };
495 
496 fn header(count: u8) [wire_header_bytes]u8 {
497     var result: [wire_header_bytes]u8 = @splat(0);
498     @memcpy(result[0..magic.len], &magic);
499     result[magic.len] = version;
500     result[result.len - 1] = count;
501     return result;
502 }
503 
504 const test_limits: Limits = .{ .max_addresses = 8 };
505 const test_capacity = Capacity.derive(test_limits) catch |err| @compileError(@errorName(err));
506 const test_alignment = Ticket.storage_alignment;
507 
508 fn mixedTicket(storage: Ticket.Storage) !Ticket {
509     var result = try Ticket.init(.{ .key = @splat(0x5a) }, storage, test_limits);
510     try result.append(.{ .ip4 = .{ .addr = .{ 1, 2, 3, 4 }, .port = 443 } });
511     try result.append(.{ .ip6 = .{
512         .addr = .{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 },
513         .port = 8443,
514     } });
515     try result.append(try Address.fromRelay("relay.example", 9443));
516     return result;
517 }
518 
519 fn expectDecodeError(expected: anyerror, source: []const u8) !void {
520     var storage: [test_capacity.storage_bytes]u8 align(test_alignment) = undefined;
521     try std.testing.expectError(expected, decode(source, &storage, test_limits));
522 }
523 
524 test "ticket storage derives caller address slots" {
525     comptime {
526         @stardustClaim(
527             alloc_phase.capacity.witness(TicketStorage, "peer_ticket_capacity"),
528             null,
529             null,
530             null,
531             null,
532             null,
533             null,
534         );
535         @stardustClaim(
536             alloc_phase.capacity.witness(TicketStorage, "peer_ticket_overload"),
537             null,
538             null,
539             null,
540             null,
541             null,
542             null,
543         );
544         @stardustClaim(
545             alloc_phase.capacity.witness(TicketStorage, "peer_ticket_transitive"),
546             null,
547             null,
548             null,
549             null,
550             null,
551             null,
552         );
553         @stardustClaim(
554             alloc_phase.capacity.witness(TicketStorage, "peer_ticket_work"),
555             null,
556             null,
557             null,
558             null,
559             null,
560             null,
561         );
562     }
563     const expected_bytes = @sizeOf(Address) * @as(usize, test_limits.max_addresses);
564     try std.testing.expectEqual(expected_bytes, test_capacity.storage_bytes);
565     var exact: [test_capacity.storage_bytes]u8 align(test_alignment) = undefined;
566     var owner = try TicketStorage.init(&exact, test_limits);
567     owner.activate();
568     const pointer = @intFromPtr(owner.storage.ptr);
569     const returned = owner.deinit();
570     try std.testing.expectEqual(pointer, @intFromPtr(returned.ptr));
571     var short: [test_capacity.storage_bytes - 1]u8 align(test_alignment) = undefined;
572     try std.testing.expectError(
573         error.StorageTooShort,
574         TicketStorage.init(&short, test_limits),
575     );
576 }
577 
578 test "ticket admits max addresses and rejects max plus one before mutation" {
579     var storage: [test_capacity.storage_bytes]u8 align(test_alignment) = undefined;
580     var ticket = try Ticket.init(.{ .key = @splat(0) }, &storage, test_limits);
581     defer _ = ticket.deinit();
582     const address = Address{ .ip4 = .{ .addr = @splat(1), .port = 1 } };
583     for (0..test_limits.max_addresses) |_| try ticket.append(address);
584     try std.testing.expectEqual(test_limits.max_addresses, ticket.items().len);
585     const before = ticket.items().len;
586     try std.testing.expectError(error.TooManyAddresses, ticket.append(address));
587     try std.testing.expectEqual(before, ticket.items().len);
588 }
589 
590 test "ticket wire codec round trips every address kind" {
591     var expected_storage: [test_capacity.storage_bytes]u8 align(test_alignment) = undefined;
592     var actual_storage: [test_capacity.storage_bytes]u8 align(test_alignment) = undefined;
593     var expected = try mixedTicket(&expected_storage);
594     defer _ = expected.deinit();
595     var wire: [test_capacity.wire_bytes]u8 = undefined;
596     const length = try encode(&expected, &wire);
597     var actual = try decode(wire[0..length], &actual_storage, test_limits);
598     defer _ = actual.deinit();
599     try std.testing.expect(expected.eql(&actual));
600 }
601 
602 test "ticket text codec round trips" {
603     var expected_storage: [test_capacity.storage_bytes]u8 align(test_alignment) = undefined;
604     var actual_storage: [test_capacity.storage_bytes]u8 align(test_alignment) = undefined;
605     var expected = try mixedTicket(&expected_storage);
606     defer _ = expected.deinit();
607     var wire: [test_capacity.wire_bytes]u8 = undefined;
608     var output: [test_capacity.text_bytes]u8 = undefined;
609     const length = try expected.formatText(&wire, &output);
610     var actual = try Ticket.parseText(
611         output[0..length],
612         &wire,
613         &actual_storage,
614         test_limits,
615     );
616     defer _ = actual.deinit();
617     try std.testing.expect(expected.eql(&actual));
618 }
619 
620 test "RFC 4648 3.5 ticket text rejects padding and noncanonical trailing bits" {
621     var ticket_storage: [test_capacity.storage_bytes]u8 align(test_alignment) = undefined;
622     var decoded_storage: [test_capacity.storage_bytes]u8 align(test_alignment) = undefined;
623     var ticket = try Ticket.init(.{ .key = @splat(0) }, &ticket_storage, test_limits);
624     defer _ = ticket.deinit();
625     var wire: [test_capacity.wire_bytes]u8 = undefined;
626     var output: [test_capacity.text_bytes + 1]u8 = undefined;
627     const length = try ticket.formatText(&wire, &output);
628     output[length] = '=';
629     try std.testing.expectError(
630         error.InvalidText,
631         Ticket.parseText(
632             output[0 .. length + 1],
633             &wire,
634             &decoded_storage,
635             test_limits,
636         ),
637     );
638     const alphabet = std.base64.url_safe_alphabet_chars;
639     const last = std.mem.indexOfScalar(u8, &alphabet, output[length - 1]).?;
640     try std.testing.expectEqual(@as(usize, 0), last % 4);
641     output[length - 1] = alphabet[last + 1];
642     try std.testing.expectError(
643         error.InvalidText,
644         Ticket.parseText(output[0..length], &wire, &decoded_storage, test_limits),
645     );
646 }
647 
648 test "eight IPv6 addresses produce 264 ticket characters" {
649     var storage: [test_capacity.storage_bytes]u8 align(test_alignment) = undefined;
650     var ticket = try Ticket.init(.{ .key = @splat(0) }, &storage, test_limits);
651     defer _ = ticket.deinit();
652     const address = Address{ .ip6 = .{ .addr = @splat(0xff), .port = 65535 } };
653     for (0..test_limits.max_addresses) |_| try ticket.append(address);
654     var wire: [test_capacity.wire_bytes]u8 = undefined;
655     var output: [test_capacity.text_bytes]u8 = undefined;
656     const length = try ticket.formatText(&wire, &output);
657     try std.testing.expectEqual(@as(usize, 264), length);
658     try std.testing.expect(length < 400);
659 }
660 
661 test "ticket decode rejects wrong magic" {
662     var source = header(0);
663     source[0] = 'X';
664     try expectDecodeError(error.WrongMagic, &source);
665 }
666 
667 test "ticket decode rejects unknown version" {
668     var source = header(0);
669     source[magic.len] = version + 1;
670     try expectDecodeError(error.UnknownVersion, &source);
671 }
672 
673 test "ticket decode rejects address count above limits" {
674     const source = header(test_limits.max_addresses + 1);
675     try expectDecodeError(error.TooManyAddresses, &source);
676 }
677 
678 test "ticket decode rejects unknown address kind" {
679     var source: [wire_header_bytes + 1]u8 = undefined;
680     source[0..wire_header_bytes].* = header(1);
681     source[wire_header_bytes] = 0xff;
682     try expectDecodeError(error.UnknownAddressKind, &source);
683 }
684 
685 test "ticket decode rejects a truncated address body" {
686     var source: [wire_header_bytes + 1]u8 = undefined;
687     source[0..wire_header_bytes].* = header(1);
688     source[wire_header_bytes] = @backingInt(Tag.ip6);
689     try expectDecodeError(error.Truncated, &source);
690 }
691 
692 test "ticket decode rejects trailing bytes" {
693     var source: [wire_header_bytes + 1]u8 = undefined;
694     source[0..wire_header_bytes].* = header(0);
695     source[wire_header_bytes] = 0;
696     try expectDecodeError(error.TrailingBytes, &source);
697 }
698 
699 test "ticket decode rejects an invalid relay host" {
700     var source: [wire_header_bytes + 5]u8 = @splat(0);
701     source[0..wire_header_bytes].* = header(1);
702     source[wire_header_bytes] = @backingInt(Tag.relay);
703     source[wire_header_bytes + 1] = 1;
704     source[wire_header_bytes + 2] = '_';
705     try expectDecodeError(error.InvalidAddress, &source);
706 }
707 
708 test "ticket decode rejects short caller storage" {
709     const source = header(0);
710     var short: [test_capacity.storage_bytes - 1]u8 align(test_alignment) = undefined;
711     try std.testing.expectError(
712         error.StorageTooShort,
713         decode(&source, &short, test_limits),
714     );
715 }