umsh/
test_vectors.rs

1//! Canonical rendering for the phase-1 protocol appendix.
2//!
3//! This module exists so the checked-in specification appendix and the
4//! implementation can be compared directly in tests.
5
6use std::fmt::Write;
7
8use crate::{
9    core::{ChannelKey, MicSize, NodeHint, PacketBuilder, PacketHeader, PublicKey},
10    crypto::{
11        CryptoEngine, DerivedChannelKeys, NodeIdentity, PairwiseKeys, SharedSecret,
12        software::{SoftwareAes, SoftwareIdentity, SoftwareSha256},
13    },
14};
15
16type SoftwareCryptoEngine = CryptoEngine<SoftwareAes, SoftwareSha256>;
17
18/// Render the canonical phase-1 test-vector appendix as Markdown.
19///
20/// The output is expected to match `docs/protocol/src/test-vectors.md`
21/// byte-for-byte.
22pub fn render_phase1_test_vectors_markdown() -> String {
23    let vectors = generate_phase1_vectors();
24    let mut out = String::new();
25
26    writeln!(&mut out, "# Test Vectors").unwrap();
27    writeln!(&mut out).unwrap();
28    writeln!(
29        &mut out,
30        "This appendix contains byte-level packet examples. All values are hexadecimal, and multi-byte numeric fields are big-endian."
31    )
32    .unwrap();
33    writeln!(&mut out).unwrap();
34    writeln!(&mut out, "## Conventions").unwrap();
35    writeln!(&mut out).unwrap();
36    writeln!(
37        &mut out,
38        "The generated examples use fixed Ed25519 private keys so the appendix covers the full path from private key to public key to X25519 ECDH to packet bytes."
39    )
40    .unwrap();
41    writeln!(&mut out).unwrap();
42    writeln!(
43        &mut out,
44        "- Node A private key: `{}`",
45        hex_words(&vectors.node_a_private)
46    )
47    .unwrap();
48    writeln!(
49        &mut out,
50        "- Node A public key: `{}`",
51        hex_words(&vectors.node_a_public.0)
52    )
53    .unwrap();
54    writeln!(
55        &mut out,
56        "  - Source hint: `{}`",
57        hex_spaced(&vectors.node_a_public.hint().0)
58    )
59    .unwrap();
60    writeln!(
61        &mut out,
62        "- Node B private key: `{}`",
63        hex_words(&vectors.node_b_private)
64    )
65    .unwrap();
66    writeln!(
67        &mut out,
68        "- Node B public key: `{}`",
69        hex_words(&vectors.node_b_public.0)
70    )
71    .unwrap();
72    writeln!(
73        &mut out,
74        "  - Destination hint: `{}`",
75        hex_spaced(&vectors.node_b_public.hint().0)
76    )
77    .unwrap();
78    writeln!(
79        &mut out,
80        "- Pairwise shared secret: `{}`",
81        hex_words(&vectors.shared_secret.0)
82    )
83    .unwrap();
84    writeln!(
85        &mut out,
86        "  - Derived from the two private keys via the implementation's Ed25519-to-X25519 conversion and X25519 ECDH."
87    )
88    .unwrap();
89
90    let unicast_encrypted_header = parse_header(&vectors.unicast_encrypted);
91    let unicast_ackreq_header = parse_header(&vectors.unicast_ackreq_fullsrc);
92    let multicast_encrypted_header = parse_header(&vectors.multicast_encrypted);
93    let multicast_authenticated_header = parse_header(&vectors.multicast_authenticated);
94    let unicast_with_options_header = parse_header(&vectors.unicast_with_options);
95    let blind_unicast_header = parse_header(&vectors.blind_unicast);
96    writeln!(
97        &mut out,
98        "- Channel key: `{}`",
99        hex_words(&vectors.channel_key.0)
100    )
101    .unwrap();
102    writeln!(
103        &mut out,
104        "- Derived channel identifier: `{}`",
105        hex_spaced(&vectors.channel.channel_id.0)
106    )
107    .unwrap();
108    writeln!(&mut out).unwrap();
109    writeln!(&mut out, "### FCF Bit Layout Reference").unwrap();
110    writeln!(&mut out).unwrap();
111    writeln!(&mut out, "```text").unwrap();
112    writeln!(&mut out, "  7   6   5   4   3   2   1   0").unwrap();
113    writeln!(&mut out, "+-------+-----------+---+---+---+").unwrap();
114    writeln!(&mut out, "| VER   | PKT TYPE  | S | R | H |").unwrap();
115    writeln!(&mut out, "+-------+-----------+---+---+---+").unwrap();
116    writeln!(&mut out, "```").unwrap();
117    writeln!(&mut out).unwrap();
118    writeln!(&mut out, "### SCF Bit Layout Reference").unwrap();
119    writeln!(&mut out).unwrap();
120    writeln!(&mut out, "```text").unwrap();
121    writeln!(&mut out, "  7   6   5   4   3   2   1   0").unwrap();
122    writeln!(&mut out, "+---+-------+---+---------------+").unwrap();
123    writeln!(&mut out, "| E |  MIC  | S |   RESERVED    |").unwrap();
124    writeln!(&mut out, "+---+-------+---+---------------+").unwrap();
125    writeln!(&mut out, "```").unwrap();
126
127    render_example(
128        &mut out,
129        "Example 1: Broadcast Beacon (S=0)",
130        "A minimal beacon with a 3-byte source hint and no payload.",
131        &[
132            (
133                "FCF",
134                "VER=3, TYPE=0 (broadcast), S=0, R=0, H=0",
135                "C0".into(),
136            ),
137            (
138                "SRC",
139                "Node A hint",
140                hex_spaced(&vectors.node_a_public.hint().0),
141            ),
142        ],
143        &vectors.broadcast_hint,
144    );
145
146    render_example(
147        &mut out,
148        "Example 2: Broadcast Beacon (S=1)",
149        "A first-contact beacon carrying the sender's full 32-byte public key.",
150        &[
151            (
152                "FCF",
153                "VER=3, TYPE=0 (broadcast), S=1, R=0, H=0",
154                "C4".into(),
155            ),
156            (
157                "SRC",
158                "Node A full key",
159                hex_spaced(&vectors.node_a_public.0),
160            ),
161        ],
162        &vectors.broadcast_full,
163    );
164
165    render_example(
166        &mut out,
167        "Example 3: Encrypted Unicast (S=0)",
168        "An encrypted unicast from Node A to Node B using source hints and frame counter 42.",
169        &[
170            ("FCF", "VER=3, TYPE=2 (unicast), S=0, R=0, H=0", "D0".into()),
171            (
172                "DST",
173                "Node B hint",
174                hex_spaced(&vectors.node_b_public.hint().0),
175            ),
176            (
177                "SRC",
178                "Node A hint",
179                hex_spaced(&vectors.node_a_public.hint().0),
180            ),
181            ("SCF", "E=1, MIC=3 (16-byte), S=0", "E0".into()),
182            ("Frame Counter", "42", hex_spaced(&42u32.to_be_bytes())),
183            (
184                "Payload",
185                "Encrypted `48 65 6C 6C 6F` (`\"Hello\"`)",
186                hex_spaced(&vectors.unicast_encrypted[unicast_encrypted_header.body_range.clone()]),
187            ),
188            (
189                "MIC",
190                "16 bytes",
191                hex_spaced(&vectors.unicast_encrypted[unicast_encrypted_header.mic_range.clone()]),
192            ),
193        ],
194        &vectors.unicast_encrypted,
195    );
196
197    render_example(
198        &mut out,
199        "Example 4: Encrypted Unicast with Ack Requested (S=1)",
200        "A first-contact encrypted unicast from Node A to Node B requesting a MAC acknowledgement. The full 32-byte source key is included.",
201        &[
202            (
203                "FCF",
204                "VER=3, TYPE=3 (unicast ack-req), S=1, R=0, H=0",
205                "DC".into(),
206            ),
207            (
208                "DST",
209                "Node B hint",
210                hex_spaced(&vectors.node_b_public.hint().0),
211            ),
212            (
213                "SRC",
214                "Node A full key",
215                hex_spaced(&vectors.node_a_public.0),
216            ),
217            ("SCF", "E=1, MIC=3 (16-byte), S=0", "E0".into()),
218            ("Frame Counter", "1", hex_spaced(&1u32.to_be_bytes())),
219            (
220                "Payload",
221                "Encrypted `68 65 79` (`\"hey\"`)",
222                hex_spaced(
223                    &vectors.unicast_ackreq_fullsrc[unicast_ackreq_header.body_range.clone()],
224                ),
225            ),
226            (
227                "MIC",
228                "16 bytes",
229                hex_spaced(
230                    &vectors.unicast_ackreq_fullsrc[unicast_ackreq_header.mic_range.clone()],
231                ),
232            ),
233        ],
234        &vectors.unicast_ackreq_fullsrc,
235    );
236
237    render_example(
238        &mut out,
239        "Example 5: Encrypted Multicast (E=1)",
240        "An encrypted multicast from Node A on channel `B08D`. The encrypted body contains the source hint followed by the plaintext payload.",
241        &[
242            (
243                "FCF",
244                "VER=3, TYPE=4 (multicast), S=0, R=0, H=0",
245                "E0".into(),
246            ),
247            (
248                "CHANNEL",
249                "Derived channel identifier",
250                hex_spaced(&vectors.channel.channel_id.0),
251            ),
252            ("SCF", "E=1, MIC=3 (16-byte), S=0", "E0".into()),
253            ("Frame Counter", "5", hex_spaced(&5u32.to_be_bytes())),
254            (
255                "Encrypted data",
256                "ENCRYPT(`SRC || \"Hello\"`)",
257                hex_spaced(
258                    &vectors.multicast_encrypted[multicast_encrypted_header.body_range.clone()],
259                ),
260            ),
261            (
262                "MIC",
263                "16 bytes",
264                hex_spaced(
265                    &vectors.multicast_encrypted[multicast_encrypted_header.mic_range.clone()],
266                ),
267            ),
268        ],
269        &vectors.multicast_encrypted,
270    );
271
272    render_example(
273        &mut out,
274        "Example 6: Authenticated Multicast (E=0)",
275        "An authenticated but unencrypted multicast from Node A carrying payload type `03` followed by `\"Hello\"`.",
276        &[
277            (
278                "FCF",
279                "VER=3, TYPE=4 (multicast), S=0, R=0, H=0",
280                "E0".into(),
281            ),
282            (
283                "CHANNEL",
284                "Derived channel identifier",
285                hex_spaced(&vectors.channel.channel_id.0),
286            ),
287            ("SCF", "E=0, MIC=3 (16-byte), S=0", "60".into()),
288            ("Frame Counter", "3", hex_spaced(&3u32.to_be_bytes())),
289            (
290                "SRC",
291                "Node A hint",
292                hex_spaced(&vectors.node_a_public.hint().0),
293            ),
294            (
295                "Payload",
296                "`03 || \"Hello\"`",
297                hex_spaced(
298                    &vectors.multicast_authenticated
299                        [multicast_authenticated_header.body_range.clone()],
300                ),
301            ),
302            (
303                "MIC",
304                "16 bytes",
305                hex_spaced(
306                    &vectors.multicast_authenticated
307                        [multicast_authenticated_header.mic_range.clone()],
308                ),
309            ),
310        ],
311        &vectors.multicast_authenticated,
312    );
313
314    writeln!(&mut out).unwrap();
315    writeln!(
316        &mut out,
317        "## Example 7: Encrypted Unicast with Options and Flood Hops"
318    )
319    .unwrap();
320    writeln!(&mut out).unwrap();
321    writeln!(&mut out, "An encrypted unicast with a region code option, an empty trace-route option, and flood hop limit 4.").unwrap();
322    writeln!(&mut out).unwrap();
323    writeln!(&mut out, "**Options encoding:**").unwrap();
324    writeln!(&mut out).unwrap();
325    writeln!(&mut out, "| Option | Number | Delta | Length | Encoding |").unwrap();
326    writeln!(&mut out, "|---|---:|---:|---:|---|").unwrap();
327    writeln!(&mut out, "| Trace Route | 2 | 2 | 0 | `20` |").unwrap();
328    writeln!(
329        &mut out,
330        "| Region Code | 11 | 9 | 2 | `92` then value `78 53` |"
331    )
332    .unwrap();
333    writeln!(&mut out, "| End marker | — | — | — | `FF` |").unwrap();
334    writeln!(&mut out).unwrap();
335    writeln!(&mut out, "| Field | Value | Hex |").unwrap();
336    writeln!(&mut out, "|---|---|---|").unwrap();
337    writeln!(
338        &mut out,
339        "| FCF | VER=3, TYPE=2 (unicast), S=0, R=0, H=1 | `D1` |"
340    )
341    .unwrap();
342    writeln!(&mut out, "| FHOPS | FHOPS_REM=4, FHOPS_ACC=0 | `40` |").unwrap();
343    writeln!(
344        &mut out,
345        "| DST | Node B hint | `{}` |",
346        hex_spaced(&vectors.node_b_public.hint().0)
347    )
348    .unwrap();
349    writeln!(
350        &mut out,
351        "| SRC | Node A hint | `{}` |",
352        hex_spaced(&vectors.node_a_public.hint().0)
353    )
354    .unwrap();
355    writeln!(&mut out, "| SCF | E=1, MIC=3 (16-byte), S=0 | `E0` |").unwrap();
356    writeln!(&mut out, "| Frame Counter | 10 | `00 00 00 0A` |").unwrap();
357    writeln!(
358        &mut out,
359        "| Options | Trace route + region code + end marker | `20 92 78 53 FF` |"
360    )
361    .unwrap();
362    writeln!(
363        &mut out,
364        "| Payload | Encrypted `68 65 79` (`\"hey\"`) | `{}` |",
365        hex_spaced(&vectors.unicast_with_options[unicast_with_options_header.body_range.clone()])
366    )
367    .unwrap();
368    writeln!(
369        &mut out,
370        "| MIC | 16 bytes | `{}` |",
371        hex_spaced(&vectors.unicast_with_options[unicast_with_options_header.mic_range.clone()])
372    )
373    .unwrap();
374    writeln!(&mut out).unwrap();
375    writeln!(&mut out, "```text").unwrap();
376    writeln!(
377        &mut out,
378        "{}",
379        wrap_hex_lines(&vectors.unicast_with_options, 16)
380    )
381    .unwrap();
382    writeln!(&mut out, "```").unwrap();
383    writeln!(&mut out).unwrap();
384    writeln!(
385        &mut out,
386        "Total: {} bytes.",
387        vectors.unicast_with_options.len()
388    )
389    .unwrap();
390
391    render_example(
392        &mut out,
393        "Example 8: Blind Unicast (S=0)",
394        "A blind unicast on channel `B08D`. The destination hint and source hint are encrypted together in `ENC_DST_SRC`, while the payload is encrypted with the blind-unicast payload keys.",
395        &[
396            (
397                "FCF",
398                "VER=3, TYPE=6 (blind unicast), S=0, R=0, H=0",
399                "F0".into(),
400            ),
401            (
402                "CHANNEL",
403                "Derived channel identifier",
404                hex_spaced(&vectors.channel.channel_id.0),
405            ),
406            ("SCF", "E=1, MIC=3 (16-byte), S=0", "E0".into()),
407            ("Frame Counter", "7", hex_spaced(&7u32.to_be_bytes())),
408            (
409                "ENC_DST_SRC",
410                "ENCRYPT(`DST || SRC`)",
411                hex_spaced(
412                    &vectors.blind_unicast[blind_unicast_header.options_range.end
413                        ..blind_unicast_header.body_range.start],
414                ),
415            ),
416            (
417                "ENC_PAYLOAD",
418                "ENCRYPT(`\"Hello\"`)",
419                hex_spaced(&vectors.blind_unicast[blind_unicast_header.body_range.clone()]),
420            ),
421            (
422                "MIC",
423                "16 bytes",
424                hex_spaced(&vectors.blind_unicast[blind_unicast_header.mic_range.clone()]),
425            ),
426        ],
427        &vectors.blind_unicast,
428    );
429
430    out
431}
432
433struct Phase1Vectors {
434    node_a_private: [u8; 32],
435    node_b_private: [u8; 32],
436    node_a_public: PublicKey,
437    node_b_public: PublicKey,
438    shared_secret: SharedSecret,
439    channel_key: ChannelKey,
440    channel: DerivedChannelKeys,
441    broadcast_hint: Vec<u8>,
442    broadcast_full: Vec<u8>,
443    unicast_encrypted: Vec<u8>,
444    unicast_ackreq_fullsrc: Vec<u8>,
445    multicast_encrypted: Vec<u8>,
446    multicast_authenticated: Vec<u8>,
447    unicast_with_options: Vec<u8>,
448    blind_unicast: Vec<u8>,
449}
450
451fn generate_phase1_vectors() -> Phase1Vectors {
452    let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
453
454    let node_a_private = [
455        0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F,
456        0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E,
457        0x2F, 0x30,
458    ];
459    let node_b_private = [
460        0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F,
461        0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E,
462        0x4F, 0x50,
463    ];
464    let node_a_identity = SoftwareIdentity::from_secret_bytes(&node_a_private);
465    let node_b_identity = SoftwareIdentity::from_secret_bytes(&node_b_private);
466    let node_a_public = *node_a_identity.public_key();
467    let node_b_public = *node_b_identity.public_key();
468    let shared_secret = node_a_identity.shared_secret_with(&node_b_public).unwrap();
469    let pairwise = engine.derive_pairwise_keys(&shared_secret);
470    let channel_key = ChannelKey([0x5A; 32]);
471    let channel = engine.derive_channel_keys(&channel_key);
472    let blind_keys = engine.derive_blind_keys(&pairwise, &channel);
473
474    Phase1Vectors {
475        node_a_private,
476        node_b_private,
477        node_a_public,
478        node_b_public,
479        shared_secret,
480        channel_key,
481        broadcast_hint: build_broadcast_hint(&node_a_public),
482        broadcast_full: build_broadcast_full(&node_a_public),
483        unicast_encrypted: build_unicast_encrypted(
484            &engine,
485            &pairwise,
486            &node_a_public,
487            node_b_public.hint(),
488        ),
489        unicast_ackreq_fullsrc: build_unicast_ackreq_fullsrc(
490            &engine,
491            &pairwise,
492            &node_a_public,
493            node_b_public.hint(),
494        ),
495        multicast_encrypted: build_multicast_encrypted(&engine, &channel, &node_a_public),
496        multicast_authenticated: build_multicast_authenticated(&engine, &channel, &node_a_public),
497        unicast_with_options: build_unicast_with_options(
498            &engine,
499            &pairwise,
500            &node_a_public,
501            node_b_public.hint(),
502        ),
503        blind_unicast: build_blind_unicast(
504            &engine,
505            &blind_keys,
506            &channel,
507            &node_a_public,
508            node_b_public.hint(),
509        ),
510        channel,
511    }
512}
513
514fn build_broadcast_hint(node_a: &PublicKey) -> Vec<u8> {
515    let mut buf = [0u8; 64];
516    PacketBuilder::new(&mut buf)
517        .broadcast()
518        .source_hint(node_a.hint())
519        .build()
520        .unwrap()
521        .to_vec()
522}
523
524fn build_broadcast_full(node_a: &PublicKey) -> Vec<u8> {
525    let mut buf = [0u8; 64];
526    PacketBuilder::new(&mut buf)
527        .broadcast()
528        .source_full(node_a)
529        .build()
530        .unwrap()
531        .to_vec()
532}
533
534fn build_unicast_encrypted(
535    engine: &SoftwareCryptoEngine,
536    pairwise: &PairwiseKeys,
537    node_a: &PublicKey,
538    dst: NodeHint,
539) -> Vec<u8> {
540    let mut buf = [0u8; 128];
541    let mut packet = PacketBuilder::new(&mut buf)
542        .unicast(dst)
543        .source_hint(node_a.hint())
544        .frame_counter(42)
545        .encrypted()
546        .mic_size(MicSize::Mic16)
547        .payload(b"Hello")
548        .build()
549        .unwrap();
550    engine.seal_packet(&mut packet, pairwise).unwrap();
551    packet.as_bytes().to_vec()
552}
553
554fn build_unicast_ackreq_fullsrc(
555    engine: &SoftwareCryptoEngine,
556    pairwise: &PairwiseKeys,
557    node_a: &PublicKey,
558    dst: NodeHint,
559) -> Vec<u8> {
560    let mut buf = [0u8; 128];
561    let mut packet = PacketBuilder::new(&mut buf)
562        .unicast(dst)
563        .source_full(node_a)
564        .frame_counter(1)
565        .ack_requested()
566        .encrypted()
567        .mic_size(MicSize::Mic16)
568        .payload(b"hey")
569        .build()
570        .unwrap();
571    engine.seal_packet(&mut packet, pairwise).unwrap();
572    packet.as_bytes().to_vec()
573}
574
575fn build_multicast_encrypted(
576    engine: &SoftwareCryptoEngine,
577    channel: &DerivedChannelKeys,
578    node_a: &PublicKey,
579) -> Vec<u8> {
580    let mut buf = [0u8; 128];
581    let mut packet = PacketBuilder::new(&mut buf)
582        .multicast(channel.channel_id)
583        .source_hint(node_a.hint())
584        .frame_counter(5)
585        .encrypted()
586        .mic_size(MicSize::Mic16)
587        .payload(b"Hello")
588        .build()
589        .unwrap();
590    let keys = PairwiseKeys {
591        k_enc: channel.k_enc,
592        k_mic: channel.k_mic,
593    };
594    engine.seal_packet(&mut packet, &keys).unwrap();
595    packet.as_bytes().to_vec()
596}
597
598fn build_multicast_authenticated(
599    engine: &SoftwareCryptoEngine,
600    channel: &DerivedChannelKeys,
601    node_a: &PublicKey,
602) -> Vec<u8> {
603    let mut buf = [0u8; 128];
604    let mut packet = PacketBuilder::new(&mut buf)
605        .multicast(channel.channel_id)
606        .source_hint(node_a.hint())
607        .frame_counter(3)
608        .mic_size(MicSize::Mic16)
609        .payload(b"\x03Hello")
610        .build()
611        .unwrap();
612    let keys = PairwiseKeys {
613        k_enc: channel.k_enc,
614        k_mic: channel.k_mic,
615    };
616    engine.seal_packet(&mut packet, &keys).unwrap();
617    packet.as_bytes().to_vec()
618}
619
620fn build_unicast_with_options(
621    engine: &SoftwareCryptoEngine,
622    pairwise: &PairwiseKeys,
623    node_a: &PublicKey,
624    dst: NodeHint,
625) -> Vec<u8> {
626    let mut buf = [0u8; 128];
627    let mut packet = PacketBuilder::new(&mut buf)
628        .unicast(dst)
629        .source_hint(node_a.hint())
630        .frame_counter(10)
631        .encrypted()
632        .flood_hops(4)
633        .trace_route()
634        .region_code([0x78, 0x53])
635        .payload(b"hey")
636        .build()
637        .unwrap();
638    engine.seal_packet(&mut packet, pairwise).unwrap();
639    packet.as_bytes().to_vec()
640}
641
642fn build_blind_unicast(
643    engine: &SoftwareCryptoEngine,
644    blind_keys: &PairwiseKeys,
645    channel: &DerivedChannelKeys,
646    node_a: &PublicKey,
647    dst: NodeHint,
648) -> Vec<u8> {
649    let mut buf = [0u8; 160];
650    let mut packet = PacketBuilder::new(&mut buf)
651        .blind_unicast(channel.channel_id, dst)
652        .source_hint(node_a.hint())
653        .frame_counter(7)
654        .mic_size(MicSize::Mic16)
655        .payload(b"Hello")
656        .build()
657        .unwrap();
658    engine
659        .seal_blind_packet(&mut packet, blind_keys, channel)
660        .unwrap();
661    packet.as_bytes().to_vec()
662}
663
664fn render_example(
665    out: &mut String,
666    title: &str,
667    description: &str,
668    fields: &[(&str, &str, String)],
669    bytes: &[u8],
670) {
671    writeln!(out).unwrap();
672    writeln!(out, "## {}", title).unwrap();
673    writeln!(out).unwrap();
674    writeln!(out, "{}", description).unwrap();
675    writeln!(out).unwrap();
676    writeln!(out, "| Field | Value | Hex |").unwrap();
677    writeln!(out, "|---|---|---|").unwrap();
678    for (field, value, hex) in fields {
679        writeln!(out, "| {} | {} | `{}` |", field, value, hex).unwrap();
680    }
681    writeln!(out).unwrap();
682    writeln!(out, "```text").unwrap();
683    writeln!(out, "{}", wrap_hex_lines(bytes, 16)).unwrap();
684    writeln!(out, "```").unwrap();
685    writeln!(out).unwrap();
686    writeln!(out, "Total: {} bytes.", bytes.len()).unwrap();
687}
688
689fn hex_spaced(bytes: &[u8]) -> String {
690    bytes
691        .iter()
692        .map(|byte| format!("{:02X}", byte))
693        .collect::<Vec<_>>()
694        .join(" ")
695}
696
697fn hex_words(bytes: &[u8]) -> String {
698    bytes
699        .chunks(2)
700        .map(|chunk| {
701            chunk
702                .iter()
703                .map(|byte| format!("{:02X}", byte))
704                .collect::<String>()
705        })
706        .collect::<Vec<_>>()
707        .join(" ")
708}
709
710fn wrap_hex_lines(bytes: &[u8], line_width: usize) -> String {
711    bytes
712        .chunks(line_width)
713        .map(hex_spaced)
714        .collect::<Vec<_>>()
715        .join("\n")
716}
717
718fn parse_header(bytes: &[u8]) -> PacketHeader {
719    PacketHeader::parse(bytes).expect("parse generated vector")
720}
721
722#[cfg(test)]
723mod tests {
724    use std::fs;
725
726    use super::render_phase1_test_vectors_markdown;
727
728    #[test]
729    fn phase1_vectors_match_protocol_appendix() {
730        let expected = render_phase1_test_vectors_markdown();
731        let actual = fs::read_to_string(concat!(
732            env!("CARGO_MANIFEST_DIR"),
733            "/../docs/protocol/src/test-vectors.md"
734        ))
735        .expect("read protocol test vectors");
736        assert_eq!(actual, expected);
737    }
738}