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 }