1pub const MAX_FULL_TX_BODY: usize = 1 << 16; pub const WIRE_VERSION: u8 = crate::protocol::WIRE_VERSION as u8;
7
8pub const PREAMBLE: &[u8; 6] = b"PLS2\x02\x00";
17
18pub const MSG_TX: u8 = 1;
21pub const MSG_HEARTBEAT: u8 = 2;
22pub const MSG_SHED: u8 = 3;
24
25pub const FLAG_ALT_INCOMPLETE: u8 = 0x01;
29
30pub const TLV_LOADED_WRITABLE: u8 = 1;
33pub const TLV_LOADED_READONLY: u8 = 2;
34pub const TLV_SERVER_TS_MS: u8 = 3;
35pub const TLV_HIGHEST_SEQ: u8 = 4;
36
37#[derive(Debug, PartialEq, Eq)]
38pub struct FrameError;
39
40pub fn put_tlv(buf: &mut Vec<u8>, t: u8, value: &[u8]) {
45 debug_assert!(value.len() <= u16::MAX as usize);
46 buf.push(t);
47 buf.extend_from_slice(&(value.len() as u16).to_le_bytes());
48 buf.extend_from_slice(value);
49}
50
51pub fn parse_tlvs(src: &[u8]) -> Result<Vec<(u8, &[u8])>, FrameError> {
58 let mut out: Vec<(u8, &[u8])> = Vec::new();
59 let mut off = 0usize;
60 while off < src.len() {
61 let t = *src.get(off).ok_or(FrameError)?;
62 let lo = *src.get(off + 1).ok_or(FrameError)?;
63 let hi = *src.get(off + 2).ok_or(FrameError)?;
64 let len = u16::from_le_bytes([lo, hi]) as usize;
65 let start = off.checked_add(3).ok_or(FrameError)?;
66 let end = start.checked_add(len).ok_or(FrameError)?;
67 let value = src.get(start..end).ok_or(FrameError)?;
68 if out.iter().any(|(seen, _)| *seen == t) {
69 return Err(FrameError);
70 }
71 out.push((t, value));
72 off = end;
73 }
74 Ok(out)
75}
76
77#[derive(Debug, Default, Clone, PartialEq, Eq)]
78pub struct FullInstruction {
79 pub program_id_index: u32,
80 pub accounts: Vec<u8>,
81 pub data: Vec<u8>,
82}
83
84#[derive(Debug, Default, Clone, PartialEq, Eq)]
85pub struct FullAtl {
86 pub account_key: [u8; 32],
87 pub writable_indexes: Vec<u8>,
88 pub readonly_indexes: Vec<u8>,
89}
90
91#[derive(Debug, Default, Clone, PartialEq, Eq)]
92pub struct FullTx {
93 pub slot: u64,
94 pub versioned: bool,
95 pub num_required_signatures: u32,
96 pub num_readonly_signed_accounts: u32,
97 pub num_readonly_unsigned_accounts: u32,
98 pub recent_blockhash: [u8; 32],
99 pub signatures: Vec<[u8; 64]>,
100 pub account_keys: Vec<[u8; 32]>,
101 pub instructions: Vec<FullInstruction>,
102 pub address_table_lookups: Vec<FullAtl>,
103}
104
105pub fn encode_full_tx(ft: &FullTx) -> Vec<u8> {
108 let mut b = Vec::with_capacity(256);
109 b.extend_from_slice(&ft.slot.to_le_bytes());
110 b.push(ft.num_required_signatures as u8);
111 b.push(ft.num_readonly_signed_accounts as u8);
112 b.push(ft.num_readonly_unsigned_accounts as u8);
113 b.push(ft.versioned as u8);
114 b.extend_from_slice(&ft.recent_blockhash);
115
116 put_u16(&mut b, ft.signatures.len());
117 for s in &ft.signatures {
118 b.extend_from_slice(s);
119 }
120 put_u16(&mut b, ft.account_keys.len());
121 for k in &ft.account_keys {
122 b.extend_from_slice(k);
123 }
124 put_u16(&mut b, ft.instructions.len());
125 for ix in &ft.instructions {
126 b.push(ix.program_id_index as u8);
127 put_u16(&mut b, ix.accounts.len());
128 b.extend_from_slice(&ix.accounts);
129 put_u16(&mut b, ix.data.len());
130 b.extend_from_slice(&ix.data);
131 }
132 put_u16(&mut b, ft.address_table_lookups.len());
133 for l in &ft.address_table_lookups {
134 b.extend_from_slice(&l.account_key);
135 put_u16(&mut b, l.writable_indexes.len());
136 b.extend_from_slice(&l.writable_indexes);
137 put_u16(&mut b, l.readonly_indexes.len());
138 b.extend_from_slice(&l.readonly_indexes);
139 }
140 b
141}
142
143pub fn decode_full_tx(src: &[u8]) -> Result<FullTx, FrameError> {
146 let (ft, consumed) = decode_full_tx_prefix(src)?;
147 if consumed != src.len() {
148 return Err(FrameError); }
150 Ok(ft)
151}
152
153pub fn decode_full_tx_prefix(src: &[u8]) -> Result<(FullTx, usize), FrameError> {
160 if src.len() > MAX_FULL_TX_BODY {
161 return Err(FrameError);
162 }
163 let mut d = Decoder { b: src, off: 0 };
164 let mut ft = FullTx {
165 slot: d.u64()?,
166 num_required_signatures: d.u8()? as u32,
167 num_readonly_signed_accounts: d.u8()? as u32,
168 num_readonly_unsigned_accounts: d.u8()? as u32,
169 versioned: d.u8()? != 0,
170 ..Default::default()
171 };
172 ft.recent_blockhash.copy_from_slice(d.take(32)?);
173
174 let n = d.count(64)?;
175 ft.signatures.reserve(n);
176 for _ in 0..n {
177 let mut s = [0u8; 64];
178 s.copy_from_slice(d.take(64)?);
179 ft.signatures.push(s);
180 }
181 let n = d.count(32)?;
182 ft.account_keys.reserve(n);
183 for _ in 0..n {
184 let mut k = [0u8; 32];
185 k.copy_from_slice(d.take(32)?);
186 ft.account_keys.push(k);
187 }
188 let n = d.count(5)?; ft.instructions.reserve(n);
190 for _ in 0..n {
191 let program_id_index = d.u8()? as u32;
192 let alen = d.u16()?;
193 let accounts = d.take(alen)?.to_vec();
194 let dlen = d.u16()?;
195 let data = d.take(dlen)?.to_vec();
196 ft.instructions.push(FullInstruction {
197 program_id_index,
198 accounts,
199 data,
200 });
201 }
202 let n = d.count(36)?; ft.address_table_lookups.reserve(n);
204 for _ in 0..n {
205 let mut account_key = [0u8; 32];
206 account_key.copy_from_slice(d.take(32)?);
207 let wlen = d.u16()?;
208 let writable_indexes = d.take(wlen)?.to_vec();
209 let rlen = d.u16()?;
210 let readonly_indexes = d.take(rlen)?.to_vec();
211 ft.address_table_lookups.push(FullAtl {
212 account_key,
213 writable_indexes,
214 readonly_indexes,
215 });
216 }
217 Ok((ft, d.off))
218}
219
220#[derive(Debug, Default, Clone, PartialEq, Eq)]
222pub struct FullTxV2 {
223 pub tx: FullTx,
224 pub alt_incomplete: bool,
225 pub loaded_writable: Vec<[u8; 32]>,
226 pub loaded_readonly: Vec<[u8; 32]>,
227}
228
229#[derive(Debug, Clone, PartialEq, Eq)]
232pub enum Frame {
233 Tx(FullTxV2),
234 Heartbeat { server_ts_ms: u64, highest_seq: u64 },
235 Unknown(u8),
236}
237
238fn flatten32(addrs: &[[u8; 32]]) -> Vec<u8> {
239 let mut v = Vec::with_capacity(addrs.len() * 32);
240 for a in addrs {
241 v.extend_from_slice(a);
242 }
243 v
244}
245
246fn unflatten32(src: &[u8]) -> Result<Vec<[u8; 32]>, FrameError> {
247 if src.len() % 32 != 0 {
248 return Err(FrameError);
249 }
250 let mut out = Vec::with_capacity(src.len() / 32);
251 for c in src.chunks_exact(32) {
252 out.push(<[u8; 32]>::try_from(c).map_err(|_| FrameError)?);
253 }
254 Ok(out)
255}
256
257pub fn encode_frame_tx(
263 ft: &FullTx,
264 alt_incomplete: bool,
265 loaded_writable: &[[u8; 32]],
266 loaded_readonly: &[[u8; 32]],
267) -> Vec<u8> {
268 let body = encode_full_tx(ft);
269 let mut b = Vec::with_capacity(body.len() + 2 + 64);
270 b.push(MSG_TX);
271 b.push(if alt_incomplete {
272 FLAG_ALT_INCOMPLETE
273 } else {
274 0
275 });
276 b.extend_from_slice(&body);
277 if !loaded_writable.is_empty() {
278 put_tlv(&mut b, TLV_LOADED_WRITABLE, &flatten32(loaded_writable));
279 }
280 if !loaded_readonly.is_empty() {
281 put_tlv(&mut b, TLV_LOADED_READONLY, &flatten32(loaded_readonly));
282 }
283 b
284}
285
286pub fn decode_frame(src: &[u8]) -> Result<Frame, FrameError> {
289 let msg_type = *src.first().ok_or(FrameError)?;
290 let flags = *src.get(1).ok_or(FrameError)?;
291 let rest = src.get(2..).ok_or(FrameError)?;
292
293 match msg_type {
294 MSG_TX => {
295 if flags & !FLAG_ALT_INCOMPLETE != 0 {
296 return Err(FrameError); }
298 let (tx, consumed) = decode_full_tx_prefix(rest)?;
301 let tlvs = parse_tlvs(rest.get(consumed..).ok_or(FrameError)?)?;
302 let mut v2 = FullTxV2 {
303 tx,
304 alt_incomplete: flags & FLAG_ALT_INCOMPLETE != 0,
305 loaded_writable: Vec::new(),
306 loaded_readonly: Vec::new(),
307 };
308 for (t, value) in tlvs {
309 match t {
310 TLV_LOADED_WRITABLE => v2.loaded_writable = unflatten32(value)?,
311 TLV_LOADED_READONLY => v2.loaded_readonly = unflatten32(value)?,
312 _ => {} }
314 }
315 Ok(Frame::Tx(v2))
316 }
317 MSG_HEARTBEAT => {
318 if flags != 0 {
324 return Err(FrameError);
325 }
326 let tlvs = parse_tlvs(rest)?;
327 let mut server_ts_ms = 0u64;
328 let mut highest_seq = 0u64;
329 for (t, value) in tlvs {
330 match t {
331 TLV_SERVER_TS_MS => {
332 server_ts_ms =
333 u64::from_le_bytes(<[u8; 8]>::try_from(value).map_err(|_| FrameError)?)
334 }
335 TLV_HIGHEST_SEQ => {
336 highest_seq =
337 u64::from_le_bytes(<[u8; 8]>::try_from(value).map_err(|_| FrameError)?)
338 }
339 _ => {}
340 }
341 }
342 Ok(Frame::Heartbeat {
343 server_ts_ms,
344 highest_seq,
345 })
346 }
347 other => Ok(Frame::Unknown(other)),
348 }
349}
350
351pub const DG_SIG_FIRST: u8 = 1;
354pub const DG_HEARTBEAT: u8 = 2;
355
356pub const DG_SIG_FIRST_MIN: usize = 1 + 8 + 8 + 64;
358pub const DG_HEARTBEAT_MIN: usize = 1 + 8 + 8;
360
361#[derive(Debug, Clone, PartialEq, Eq)]
362pub enum Datagram {
363 SigFirst {
364 slot: u64,
365 seq: u64,
366 signature: [u8; 64],
367 },
368 Heartbeat {
369 server_ts_ms: u64,
370 highest_seq: u64,
371 },
372 Unknown(u8),
373}
374
375pub fn encode_dg_sig_first(buf: &mut [u8; DG_SIG_FIRST_MIN], slot: u64, seq: u64, sig: &[u8; 64]) {
376 buf[0] = DG_SIG_FIRST;
377 buf[1..9].copy_from_slice(&slot.to_le_bytes());
378 buf[9..17].copy_from_slice(&seq.to_le_bytes());
379 buf[17..81].copy_from_slice(sig);
380}
381
382pub fn encode_dg_heartbeat(buf: &mut [u8; DG_HEARTBEAT_MIN], server_ts_ms: u64, highest_seq: u64) {
383 buf[0] = DG_HEARTBEAT;
384 buf[1..9].copy_from_slice(&server_ts_ms.to_le_bytes());
385 buf[9..17].copy_from_slice(&highest_seq.to_le_bytes());
386}
387
388pub fn decode_datagram(src: &[u8]) -> Option<Datagram> {
395 match *src.first()? {
396 DG_SIG_FIRST if src.len() >= DG_SIG_FIRST_MIN => Some(Datagram::SigFirst {
397 slot: u64::from_le_bytes(src.get(1..9)?.try_into().ok()?),
398 seq: u64::from_le_bytes(src.get(9..17)?.try_into().ok()?),
399 signature: src.get(17..81)?.try_into().ok()?,
400 }),
401 DG_HEARTBEAT if src.len() >= DG_HEARTBEAT_MIN => Some(Datagram::Heartbeat {
402 server_ts_ms: u64::from_le_bytes(src.get(1..9)?.try_into().ok()?),
403 highest_seq: u64::from_le_bytes(src.get(9..17)?.try_into().ok()?),
404 }),
405 DG_SIG_FIRST | DG_HEARTBEAT => None, other => Some(Datagram::Unknown(other)),
407 }
408}
409
410fn put_u16(b: &mut Vec<u8>, n: usize) {
411 b.extend_from_slice(&(n as u16).to_le_bytes());
412}
413
414struct Decoder<'a> {
415 b: &'a [u8],
416 off: usize,
417}
418
419impl<'a> Decoder<'a> {
420 fn take(&mut self, n: usize) -> Result<&'a [u8], FrameError> {
421 let end = self.off.checked_add(n).ok_or(FrameError)?;
422 if end > self.b.len() {
423 return Err(FrameError);
424 }
425 let s = &self.b[self.off..end];
426 self.off = end;
427 Ok(s)
428 }
429 fn u8(&mut self) -> Result<u8, FrameError> {
430 Ok(self.take(1)?[0])
431 }
432 fn u16(&mut self) -> Result<usize, FrameError> {
433 let s = self.take(2)?;
434 Ok(u16::from_le_bytes([s[0], s[1]]) as usize)
435 }
436 fn u64(&mut self) -> Result<u64, FrameError> {
437 let s = self.take(8)?;
438 Ok(u64::from_le_bytes(s.try_into().unwrap()))
439 }
440 fn count(&mut self, min_elem: usize) -> Result<usize, FrameError> {
443 let n = self.u16()?;
444 if min_elem == 0 || n > (self.b.len() - self.off) / min_elem {
445 return Err(FrameError);
446 }
447 Ok(n)
448 }
449}
450
451#[cfg(test)]
452mod tests {
453 use super::*;
454
455 fn sample(versioned: bool) -> FullTx {
456 FullTx {
457 slot: 7,
458 versioned,
459 num_required_signatures: 1,
460 recent_blockhash: [0xCC; 32],
461 signatures: vec![[7u8; 64]],
462 account_keys: vec![[0xA1; 32]],
463 instructions: vec![FullInstruction {
464 program_id_index: 1,
465 accounts: vec![0],
466 data: vec![0xDE, 0xAD, 0xBE],
467 }],
468 address_table_lookups: if versioned {
469 vec![FullAtl {
470 account_key: [0xEE; 32],
471 writable_indexes: vec![5],
472 readonly_indexes: vec![7],
473 }]
474 } else {
475 vec![]
476 },
477 ..Default::default()
478 }
479 }
480
481 #[test]
482 fn full_tx_round_trip_legacy_and_v0() {
483 for versioned in [false, true] {
484 let ft = sample(versioned);
485 let body = encode_full_tx(&ft);
486 let got = decode_full_tx(&body).unwrap();
487 assert_eq!(got, ft);
488 }
489 }
490
491 #[test]
492 fn full_tx_rejects_truncated() {
493 let body = encode_full_tx(&sample(true));
494 assert_eq!(decode_full_tx(&body[..body.len() - 3]), Err(FrameError));
495 }
496
497 #[test]
498 fn preamble_is_six_bytes_and_starts_nonzero() {
499 assert_eq!(PREAMBLE.len(), 6);
503 assert_ne!(PREAMBLE[0], 0x00);
504 assert_eq!(&PREAMBLE[0..4], b"PLS2");
505 assert_eq!(PREAMBLE[4], WIRE_VERSION);
506 assert_eq!(PREAMBLE[5], 0);
507 }
508
509 #[test]
510 fn tlv_round_trips_in_order() {
511 let mut b = Vec::new();
512 put_tlv(&mut b, TLV_LOADED_WRITABLE, &[1u8; 64]);
513 put_tlv(&mut b, TLV_LOADED_READONLY, &[2u8; 32]);
514 let got = parse_tlvs(&b).expect("valid");
515 assert_eq!(got.len(), 2);
516 assert_eq!(got[0].0, TLV_LOADED_WRITABLE);
517 assert_eq!(got[0].1, &[1u8; 64][..]);
518 assert_eq!(got[1].0, TLV_LOADED_READONLY);
519 assert_eq!(got[1].1, &[2u8; 32][..]);
520 }
521
522 #[test]
523 fn tlv_length_is_u16_so_a_large_value_fits() {
524 let big = vec![7u8; 3200];
526 let mut b = Vec::new();
527 put_tlv(&mut b, TLV_LOADED_WRITABLE, &big);
528 let got = parse_tlvs(&b).expect("valid");
529 assert_eq!(got[0].1.len(), 3200);
530 }
531
532 #[test]
533 fn tlv_unknown_type_is_kept_for_the_caller_to_skip() {
534 let mut b = Vec::new();
535 put_tlv(&mut b, 200, &[9u8; 4]);
536 let got = parse_tlvs(&b).expect("unknown types parse fine");
537 assert_eq!(got[0].0, 200);
538 }
539
540 #[test]
541 fn tlv_duplicate_type_is_rejected() {
542 let mut b = Vec::new();
543 put_tlv(&mut b, TLV_LOADED_WRITABLE, &[1u8; 32]);
544 put_tlv(&mut b, TLV_LOADED_WRITABLE, &[2u8; 32]);
545 assert_eq!(parse_tlvs(&b), Err(FrameError));
546 }
547
548 #[test]
549 fn tlv_truncated_is_rejected() {
550 let mut b = Vec::new();
551 put_tlv(&mut b, TLV_LOADED_WRITABLE, &[1u8; 32]);
552 for cut in 1..b.len() {
553 assert_eq!(parse_tlvs(&b[..cut]), Err(FrameError), "cut={cut}");
554 }
555 assert_eq!(parse_tlvs(&[]), Ok(Vec::new()));
556 }
557
558 #[test]
559 fn tlv_length_overrunning_the_buffer_is_rejected() {
560 let b = vec![1u8, 0xFF, 0xFF];
562 assert_eq!(parse_tlvs(&b), Err(FrameError));
563 }
564
565 fn sample_fulltx() -> FullTx {
566 FullTx {
567 slot: 438_690_000,
568 versioned: true,
569 num_required_signatures: 1,
570 num_readonly_signed_accounts: 0,
571 num_readonly_unsigned_accounts: 1,
572 recent_blockhash: [0xCC; 32],
573 signatures: vec![[7u8; 64]],
574 account_keys: vec![[0xA1; 32], [0xB2; 32]],
575 instructions: vec![FullInstruction {
576 program_id_index: 1,
577 accounts: vec![0],
578 data: vec![9, 9],
579 }],
580 address_table_lookups: vec![FullAtl {
581 account_key: [0xEE; 32],
582 writable_indexes: vec![0],
583 readonly_indexes: vec![1],
584 }],
585 }
586 }
587
588 #[test]
589 fn v2_tx_frame_round_trips_bare() {
590 let tx = sample_fulltx();
591 let enc = encode_frame_tx(&tx, false, &[], &[]);
592 assert_eq!(enc[0], MSG_TX);
593 assert_eq!(enc[1], 0, "no flags set");
594 match decode_frame(&enc).expect("valid") {
595 Frame::Tx(v2) => {
596 assert_eq!(v2.tx, tx);
597 assert!(!v2.alt_incomplete);
598 assert!(v2.loaded_writable.is_empty());
599 assert!(v2.loaded_readonly.is_empty());
600 }
601 other => panic!("expected Tx, got {other:?}"),
602 }
603 }
604
605 #[test]
606 fn v2_tx_frame_round_trips_enriched() {
607 let tx = sample_fulltx();
608 let w = [[0x11u8; 32], [0x22u8; 32]];
609 let r = [[0x33u8; 32]];
610 let enc = encode_frame_tx(&tx, true, &w, &r);
611 assert_eq!(enc[1] & FLAG_ALT_INCOMPLETE, FLAG_ALT_INCOMPLETE);
612 match decode_frame(&enc).expect("valid") {
613 Frame::Tx(v2) => {
614 assert!(v2.alt_incomplete);
615 assert_eq!(v2.loaded_writable, w.to_vec());
616 assert_eq!(v2.loaded_readonly, r.to_vec());
617 }
618 other => panic!("expected Tx, got {other:?}"),
619 }
620 }
621
622 #[test]
623 fn v2_body_is_byte_identical_to_v1_encoding() {
624 let tx = sample_fulltx();
626 let v1 = encode_full_tx(&tx);
627 let v2 = encode_frame_tx(&tx, false, &[], &[]);
628 assert_eq!(&v2[2..2 + v1.len()], &v1[..]);
629 }
630
631 #[test]
632 fn unknown_msg_type_is_reported_not_rejected() {
633 let mut enc = encode_frame_tx(&sample_fulltx(), false, &[], &[]);
634 enc[0] = 99;
635 match decode_frame(&enc).expect("unknown type must not error") {
636 Frame::Unknown(99) => {}
637 other => panic!("expected Unknown(99), got {other:?}"),
638 }
639 }
640
641 #[test]
642 fn reserved_flag_bits_are_rejected() {
643 let mut enc = encode_frame_tx(&sample_fulltx(), false, &[], &[]);
644 enc[1] = 0x02; assert_eq!(decode_frame(&enc), Err(FrameError));
646 }
647
648 #[test]
649 fn loaded_address_tlv_with_a_non_multiple_of_32_is_rejected() {
650 let mut enc = Vec::new();
651 enc.push(MSG_TX);
652 enc.push(0);
653 enc.extend_from_slice(&encode_full_tx(&sample_fulltx()));
654 put_tlv(&mut enc, TLV_LOADED_WRITABLE, &[0u8; 33]);
655 assert_eq!(decode_frame(&enc), Err(FrameError));
656 }
657
658 #[test]
659 fn heartbeat_frame_round_trips() {
660 let mut enc = Vec::new();
661 enc.push(MSG_HEARTBEAT);
662 enc.push(0);
663 put_tlv(
664 &mut enc,
665 TLV_SERVER_TS_MS,
666 &1_700_000_000_123u64.to_le_bytes(),
667 );
668 put_tlv(&mut enc, TLV_HIGHEST_SEQ, &4242u64.to_le_bytes());
669 match decode_frame(&enc).expect("valid") {
670 Frame::Heartbeat {
671 server_ts_ms,
672 highest_seq,
673 } => {
674 assert_eq!(server_ts_ms, 1_700_000_000_123);
675 assert_eq!(highest_seq, 4242);
676 }
677 other => panic!("expected Heartbeat, got {other:?}"),
678 }
679 }
680
681 #[test]
682 fn heartbeat_frame_rejects_any_nonzero_flags() {
683 for flags in [FLAG_ALT_INCOMPLETE, 0x02, 0xFF] {
686 let mut enc = Vec::new();
687 enc.push(MSG_HEARTBEAT);
688 enc.push(flags);
689 put_tlv(&mut enc, TLV_SERVER_TS_MS, &1u64.to_le_bytes());
690 assert_eq!(decode_frame(&enc), Err(FrameError), "flags={flags:#x}");
691 }
692 }
693
694 #[test]
695 fn frame_too_short_is_rejected() {
696 assert_eq!(decode_frame(&[]), Err(FrameError));
697 assert_eq!(decode_frame(&[MSG_TX]), Err(FrameError));
698 }
699
700 #[test]
701 fn dg_sig_first_round_trips() {
702 let mut buf = [0u8; DG_SIG_FIRST_MIN];
703 encode_dg_sig_first(&mut buf, 438_690_000, 12345, &[9u8; 64]);
704 assert_eq!(buf[0], DG_SIG_FIRST);
705 match decode_datagram(&buf).expect("valid") {
706 Datagram::SigFirst {
707 slot,
708 seq,
709 signature,
710 } => {
711 assert_eq!(slot, 438_690_000);
712 assert_eq!(seq, 12345);
713 assert_eq!(signature, [9u8; 64]);
714 }
715 other => panic!("expected SigFirst, got {other:?}"),
716 }
717 }
718
719 #[test]
720 fn dg_heartbeat_round_trips() {
721 let mut buf = [0u8; DG_HEARTBEAT_MIN];
722 encode_dg_heartbeat(&mut buf, 1_700_000_000_123, 999);
723 match decode_datagram(&buf).expect("valid") {
724 Datagram::Heartbeat {
725 server_ts_ms,
726 highest_seq,
727 } => {
728 assert_eq!(server_ts_ms, 1_700_000_000_123);
729 assert_eq!(highest_seq, 999);
730 }
731 other => panic!("expected Heartbeat, got {other:?}"),
732 }
733 }
734
735 #[test]
736 fn dg_minimum_length_not_exact_length() {
737 let mut buf = [0u8; DG_SIG_FIRST_MIN];
741 encode_dg_sig_first(&mut buf, 7, 8, &[3u8; 64]);
742 let mut longer = buf.to_vec();
743 longer.extend_from_slice(&[0xAB; 16]);
744 match decode_datagram(&longer).expect("trailing bytes must be ignored") {
745 Datagram::SigFirst { slot, seq, .. } => {
746 assert_eq!(slot, 7);
747 assert_eq!(seq, 8);
748 }
749 other => panic!("expected SigFirst, got {other:?}"),
750 }
751 }
752
753 #[test]
754 fn dg_below_minimum_is_rejected() {
755 let mut buf = [0u8; DG_SIG_FIRST_MIN];
756 encode_dg_sig_first(&mut buf, 1, 2, &[0u8; 64]);
757 assert!(decode_datagram(&buf[..DG_SIG_FIRST_MIN - 1]).is_none());
758 let mut hb = [0u8; DG_HEARTBEAT_MIN];
759 encode_dg_heartbeat(&mut hb, 1, 2);
760 assert!(decode_datagram(&hb[..DG_HEARTBEAT_MIN - 1]).is_none());
761 assert!(decode_datagram(&[]).is_none());
762 }
763
764 #[test]
765 fn dg_unknown_type_is_reported_not_rejected() {
766 let buf = [200u8, 1, 2, 3];
767 match decode_datagram(&buf).expect("unknown type must not be an error") {
768 Datagram::Unknown(200) => {}
769 other => panic!("expected Unknown(200), got {other:?}"),
770 }
771 }
772
773 #[test]
774 fn a_v1_72_byte_datagram_is_rejected_by_the_length_rule() {
775 let v1 = [1u8; 72];
780 assert_eq!(decode_datagram(&v1), None);
781 }
782}