Skip to main content

sumchain_primitives/
messaging.rs

1//! SRC-201: On-Chain Messaging Token Standard
2//!
3//! Defines types for encrypted on-chain messaging with registry-as-recipient
4//! pattern for metadata privacy. Messages are stored in transaction calldata.
5
6use serde::{Deserialize, Serialize};
7use serde_big_array::BigArray;
8
9use crate::{Address, Balance, Hash};
10
11/// SRC-201 magic bytes: "S201" in ASCII
12pub const SRC201_MAGIC: [u8; 4] = [0x53, 0x32, 0x30, 0x31];
13
14/// Current SRC-201 protocol version
15pub const SRC201_VERSION: u8 = 1;
16
17/// Fixed header size in bytes
18pub const SRC201_HEADER_SIZE: usize = 72;
19
20/// Nonce size for XChaCha20-Poly1305
21pub const SRC201_NONCE_SIZE: usize = 24;
22
23/// Auth tag size for Poly1305
24pub const SRC201_TAG_SIZE: usize = 16;
25
26/// KDF context for message key derivation
27pub const SRC201_KDF_CONTEXT: &str = "SRC-201-v1.1-message-key";
28
29/// KDF context for attachment key derivation
30pub const SRC201_ATTACHMENT_KDF_CONTEXT: &str = "SRC-201-v1.1-attachment-key";
31
32/// Default daily message quota per address
33pub const DEFAULT_DAILY_QUOTA: u32 = 100;
34
35/// Default maximum message size in bytes
36pub const DEFAULT_MAX_MESSAGE_SIZE: u32 = 65535;
37
38/// Default minimum stake for trusted sender tier (100 Koppa in base units)
39pub const DEFAULT_MIN_TRUST_STAKE: u128 = 100_000_000_000;
40
41/// Spam score threshold for restrictions
42pub const DEFAULT_SPAM_THRESHOLD: u32 = 50;
43
44/// High spam score threshold requiring stake
45pub const DEFAULT_HIGH_SPAM_THRESHOLD: u32 = 80;
46
47/// Messaging operation codes
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
49#[repr(u8)]
50pub enum MessagingOperation {
51    /// Send message with gas sponsorship (meta-transaction)
52    SendMessage = 0,
53    /// Send message directly (user pays gas)
54    SendMessageDirect = 1,
55    /// Send message with attached Koppa payment
56    SendMessageWithPayment = 2,
57    /// Claim payment from a message (called by recipient)
58    ClaimPayment = 3,
59    /// Stake Koppa for trusted sender tier
60    StakeForTrust = 4,
61    /// Withdraw stake (with cooldown)
62    Unstake = 5,
63    /// Set inbox filter mode
64    SetInboxFilter = 6,
65    /// Add address to contacts whitelist
66    AddContact = 7,
67    /// Remove address from contacts
68    RemoveContact = 8,
69    /// Block a sender
70    BlockSender = 9,
71    /// Report a message as spam
72    ReportSpam = 10,
73    /// Register Ed25519 public key for messaging
74    RegisterPublicKey = 11,
75    /// Update registered public key
76    UpdatePublicKey = 12,
77
78    // Admin operations (governance controlled, 128+)
79    /// Set daily free message quota
80    SetDailyQuota = 128,
81    /// Set maximum message size
82    SetMaxMessageSize = 129,
83    /// Set minimum stake for trusted tier
84    SetMinTrustStake = 130,
85    /// Enable/disable gas sponsorship
86    SetSponsorshipEnabled = 131,
87    /// Fund the registry with Koppa
88    FundRegistry = 132,
89}
90
91impl MessagingOperation {
92    /// Convert from byte
93    pub fn from_byte(b: u8) -> Option<Self> {
94        match b {
95            0 => Some(MessagingOperation::SendMessage),
96            1 => Some(MessagingOperation::SendMessageDirect),
97            2 => Some(MessagingOperation::SendMessageWithPayment),
98            3 => Some(MessagingOperation::ClaimPayment),
99            4 => Some(MessagingOperation::StakeForTrust),
100            5 => Some(MessagingOperation::Unstake),
101            6 => Some(MessagingOperation::SetInboxFilter),
102            7 => Some(MessagingOperation::AddContact),
103            8 => Some(MessagingOperation::RemoveContact),
104            9 => Some(MessagingOperation::BlockSender),
105            10 => Some(MessagingOperation::ReportSpam),
106            11 => Some(MessagingOperation::RegisterPublicKey),
107            12 => Some(MessagingOperation::UpdatePublicKey),
108            128 => Some(MessagingOperation::SetDailyQuota),
109            129 => Some(MessagingOperation::SetMaxMessageSize),
110            130 => Some(MessagingOperation::SetMinTrustStake),
111            131 => Some(MessagingOperation::SetSponsorshipEnabled),
112            132 => Some(MessagingOperation::FundRegistry),
113            _ => None,
114        }
115    }
116
117    /// Check if this is an admin operation
118    pub fn is_admin(&self) -> bool {
119        (*self as u8) >= 128
120    }
121
122    /// Check if this operation sends a message
123    pub fn is_send(&self) -> bool {
124        matches!(
125            self,
126            MessagingOperation::SendMessage
127                | MessagingOperation::SendMessageDirect
128                | MessagingOperation::SendMessageWithPayment
129        )
130    }
131}
132
133/// Messaging transaction data
134#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
135pub struct MessagingTxData {
136    /// Operation code
137    pub operation: MessagingOperation,
138    /// Operation-specific data (serialized)
139    pub data: Vec<u8>,
140}
141
142/// Message flags byte
143#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
144pub struct MessageFlags(pub u8);
145
146impl MessageFlags {
147    pub const ENCRYPTED: u8 = 1 << 0;
148    pub const HAS_REPLY_TO: u8 = 1 << 1;
149    pub const HAS_TIMESTAMP: u8 = 1 << 2;
150    pub const HAS_ATTACHMENTS: u8 = 1 << 3;
151    pub const IS_READ_RECEIPT: u8 = 1 << 4;
152    pub const IS_PAYMENT_REQUEST: u8 = 1 << 5;
153    pub const REQUIRES_STAKE: u8 = 1 << 6;
154
155    pub fn new() -> Self {
156        Self(0)
157    }
158
159    pub fn encrypted() -> Self {
160        Self(Self::ENCRYPTED)
161    }
162
163    pub fn is_encrypted(&self) -> bool {
164        self.0 & Self::ENCRYPTED != 0
165    }
166
167    pub fn has_reply_to(&self) -> bool {
168        self.0 & Self::HAS_REPLY_TO != 0
169    }
170
171    pub fn has_timestamp(&self) -> bool {
172        self.0 & Self::HAS_TIMESTAMP != 0
173    }
174
175    pub fn has_attachments(&self) -> bool {
176        self.0 & Self::HAS_ATTACHMENTS != 0
177    }
178
179    pub fn is_read_receipt(&self) -> bool {
180        self.0 & Self::IS_READ_RECEIPT != 0
181    }
182
183    pub fn is_payment_request(&self) -> bool {
184        self.0 & Self::IS_PAYMENT_REQUEST != 0
185    }
186
187    pub fn requires_stake(&self) -> bool {
188        self.0 & Self::REQUIRES_STAKE != 0
189    }
190
191    pub fn set(&mut self, flag: u8) {
192        self.0 |= flag;
193    }
194
195    pub fn clear(&mut self, flag: u8) {
196        self.0 &= !flag;
197    }
198}
199
200/// Content type for message payload
201#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
202#[repr(u8)]
203pub enum ContentType {
204    // Text types (0x01-0x0F)
205    TextPlain = 0x01,
206    TextMarkdown = 0x02,
207    TextHtml = 0x03,
208
209    // Application types (0x10-0x2F)
210    ApplicationJson = 0x10,
211    ApplicationPdf = 0x11,
212
213    // Image types (0x30-0x3F)
214    ImagePng = 0x30,
215    ImageJpeg = 0x31,
216    ImageGif = 0x32,
217    ImageWebp = 0x33,
218
219    // SUM-specific types (0x80-0x8F)
220    PaymentRequest = 0x80,
221    ReadReceipt = 0x81,
222    ContactCard = 0x82,
223
224    // Custom type (0xFF)
225    Custom = 0xFF,
226}
227
228impl ContentType {
229    pub fn from_byte(b: u8) -> Option<Self> {
230        match b {
231            0x01 => Some(ContentType::TextPlain),
232            0x02 => Some(ContentType::TextMarkdown),
233            0x03 => Some(ContentType::TextHtml),
234            0x10 => Some(ContentType::ApplicationJson),
235            0x11 => Some(ContentType::ApplicationPdf),
236            0x30 => Some(ContentType::ImagePng),
237            0x31 => Some(ContentType::ImageJpeg),
238            0x32 => Some(ContentType::ImageGif),
239            0x33 => Some(ContentType::ImageWebp),
240            0x80 => Some(ContentType::PaymentRequest),
241            0x81 => Some(ContentType::ReadReceipt),
242            0x82 => Some(ContentType::ContactCard),
243            0xFF => Some(ContentType::Custom),
244            _ => None,
245        }
246    }
247
248    pub fn is_text(&self) -> bool {
249        (*self as u8) >= 0x01 && (*self as u8) <= 0x0F
250    }
251
252    pub fn is_image(&self) -> bool {
253        (*self as u8) >= 0x30 && (*self as u8) <= 0x3F
254    }
255}
256
257impl Default for ContentType {
258    fn default() -> Self {
259        ContentType::TextPlain
260    }
261}
262
263/// Inbox filter mode
264#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
265#[repr(u8)]
266pub enum InboxFilter {
267    /// Accept all messages
268    #[default]
269    AcceptAll = 0,
270    /// Only accept from contacts
271    ContactsOnly = 1,
272    /// Only accept from staked senders
273    StakedOnly = 2,
274}
275
276impl InboxFilter {
277    pub fn from_byte(b: u8) -> Option<Self> {
278        match b {
279            0 => Some(InboxFilter::AcceptAll),
280            1 => Some(InboxFilter::ContactsOnly),
281            2 => Some(InboxFilter::StakedOnly),
282            _ => None,
283        }
284    }
285}
286
287/// Attachment part type
288#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
289#[repr(u8)]
290pub enum AttachmentType {
291    /// Inline data (encrypted in message)
292    Inline = 0x01,
293    /// External reference (IPFS, Arweave, etc.)
294    External = 0x02,
295}
296
297/// External storage protocol
298#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
299#[repr(u8)]
300pub enum ExternalProtocol {
301    IPFS = 0x01,
302    Arweave = 0x02,
303    HTTPS = 0x03,
304}
305
306/// Pending payment information
307#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
308pub struct PendingPayment {
309    /// Hash of recipient address (for verification)
310    pub recipient_hash: [u8; 32],
311    /// Payment amount in Koppa
312    pub amount: Balance,
313    /// Expiry timestamp (Unix)
314    pub expiry: u64,
315    /// Sender address (for refunds)
316    pub sender: Address,
317}
318
319/// Message event for indexing (emitted by registry)
320#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
321pub struct MessageEvent {
322    /// Sender address (visible on-chain)
323    pub sender: Address,
324    /// BLAKE3 hash of recipient address
325    pub recipient_hash: [u8; 32],
326    /// Transaction hash (message ID)
327    pub message_id: Hash,
328    /// Message size in bytes
329    pub size: u32,
330    /// Whether message has attached payment
331    pub has_payment: bool,
332    /// Block height when message was included
333    pub block_height: u64,
334    /// Block timestamp
335    pub timestamp: u64,
336}
337
338/// Quota information for a sender
339#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
340pub struct QuotaInfo {
341    /// Messages used today
342    pub used_today: u32,
343    /// Remaining messages today
344    pub remaining: u32,
345    /// Total daily quota
346    pub total_quota: u32,
347    /// Sender tier (0=basic, 1=staked)
348    pub tier: u8,
349    /// Stake amount
350    pub stake_amount: Balance,
351    /// Unix timestamp when quota resets
352    pub resets_at: u64,
353}
354
355/// Spam report for a message
356#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
357pub struct SpamReport {
358    /// Reporter address
359    pub reporter: Address,
360    /// Timestamp of report
361    pub timestamp: u64,
362    /// Message ID being reported
363    pub message_id: Hash,
364}
365
366// ============================================================================
367// Operation-specific data structures
368// ============================================================================
369
370/// Data for SendMessage operation
371#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
372pub struct SendMessageData {
373    /// Encoded SRC-201 message (encrypted)
374    pub message_data: Vec<u8>,
375    /// BLAKE3 hash of recipient address (for indexing)
376    pub recipient_hash: [u8; 32],
377}
378
379/// Data for SendMessageWithPayment operation
380#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
381pub struct SendMessageWithPaymentData {
382    /// Encoded SRC-201 message (encrypted)
383    pub message_data: Vec<u8>,
384    /// BLAKE3 hash of recipient address
385    pub recipient_hash: [u8; 32],
386    /// Koppa amount to attach
387    pub koppa_amount: Balance,
388}
389
390/// Data for ClaimPayment operation
391#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
392pub struct ClaimPaymentData {
393    /// Message ID (tx hash) containing the payment
394    pub message_id: Hash,
395    /// Recipient's address (proves ownership)
396    pub recipient_address: Address,
397}
398
399/// Data for StakeForTrust operation
400#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
401pub struct StakeForTrustData {
402    /// Amount to stake
403    pub amount: Balance,
404}
405
406/// Data for Unstake operation
407#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
408pub struct UnstakeData {
409    /// Amount to unstake
410    pub amount: Balance,
411}
412
413/// Data for SetInboxFilter operation
414#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
415pub struct SetInboxFilterData {
416    /// Filter mode
417    pub mode: InboxFilter,
418}
419
420/// Data for AddContact/RemoveContact operations
421#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
422pub struct ContactData {
423    /// BLAKE3 hash of contact's address
424    pub contact_hash: [u8; 32],
425}
426
427/// Data for BlockSender operation
428#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
429pub struct BlockSenderData {
430    /// Address to block
431    pub sender: Address,
432}
433
434/// Data for ReportSpam operation
435#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
436pub struct ReportSpamData {
437    /// Message ID being reported
438    pub message_id: Hash,
439    /// Address of the spammer
440    pub spammer: Address,
441}
442
443/// Data for SetDailyQuota admin operation
444#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
445pub struct SetDailyQuotaData {
446    pub quota: u32,
447}
448
449/// Data for SetMaxMessageSize admin operation
450#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
451pub struct SetMaxMessageSizeData {
452    pub size: u32,
453}
454
455/// Data for SetMinTrustStake admin operation
456#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
457pub struct SetMinTrustStakeData {
458    pub amount: Balance,
459}
460
461/// Data for SetSponsorshipEnabled admin operation
462#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
463pub struct SetSponsorshipEnabledData {
464    pub enabled: bool,
465}
466
467/// Data for FundRegistry admin operation
468#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
469pub struct FundRegistryData {
470    pub amount: Balance,
471}
472
473/// Data for RegisterPublicKey operation
474#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
475pub struct RegisterPublicKeyData {
476    /// Ed25519 public key (32 bytes)
477    pub public_key: [u8; 32],
478}
479
480/// Data for UpdatePublicKey operation
481#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
482pub struct UpdatePublicKeyData {
483    /// New Ed25519 public key (32 bytes)
484    pub new_public_key: [u8; 32],
485}
486
487/// Registered public key entry
488#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
489pub struct RegisteredPublicKey {
490    /// The Ed25519 public key
491    pub public_key: [u8; 32],
492    /// Address that registered this key
493    pub address: Address,
494    /// Block height when registered
495    pub registered_at_block: u64,
496    /// Timestamp when registered
497    pub registered_at: u64,
498    /// Block height when last updated (0 if never updated)
499    pub updated_at_block: u64,
500}
501
502// ============================================================================
503// Sponsored message (meta-transaction)
504// ============================================================================
505
506/// Sponsored message for gas-free sending
507#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
508pub struct SponsoredMessage {
509    /// Encoded SRC-201 message
510    pub message_data: Vec<u8>,
511    /// BLAKE3 hash of recipient address
512    pub recipient_hash: [u8; 32],
513    /// Sender's signature over the message envelope
514    #[serde(with = "BigArray")]
515    pub signature: [u8; 64],
516    /// Sender's public key
517    pub sender_pubkey: [u8; 32],
518    /// Sender's message nonce (prevents replay)
519    pub nonce: u64,
520    /// Expiry timestamp (Unix)
521    pub expiry: u64,
522    /// Optional Koppa amount to attach
523    pub koppa_amount: Option<Balance>,
524}
525
526impl SponsoredMessage {
527    /// Compute the signing hash for this sponsored message
528    pub fn signing_hash(&self, chain_id: u64, registry_address: &Address) -> Hash {
529        // Domain separator
530        let mut domain_data = Vec::new();
531        domain_data.extend_from_slice(b"SRC-201-v1.1");
532        domain_data.extend_from_slice(&chain_id.to_be_bytes());
533        domain_data.extend_from_slice(registry_address.as_bytes());
534        let domain_separator = blake3::hash(&domain_data);
535
536        // Message hash
537        let message_hash = blake3::hash(&self.message_data);
538
539        let mut data = Vec::new();
540        data.extend_from_slice(domain_separator.as_bytes());
541        data.extend_from_slice(&self.sender_pubkey);
542        data.extend_from_slice(&self.recipient_hash);
543        data.extend_from_slice(message_hash.as_bytes());
544        data.extend_from_slice(&self.nonce.to_be_bytes());
545        data.extend_from_slice(&self.expiry.to_be_bytes());
546        if let Some(amount) = self.koppa_amount {
547            data.extend_from_slice(&amount.to_be_bytes());
548        }
549
550        Hash::hash(&data)
551    }
552}
553
554// ============================================================================
555// Message header structure (for parsing)
556// ============================================================================
557
558/// Parsed SRC-201 message header (72 bytes)
559#[derive(Debug, Clone, PartialEq, Eq)]
560pub struct MessageHeader {
561    pub magic: [u8; 4],
562    pub version: u8,
563    pub flags: MessageFlags,
564    pub content_type: ContentType,
565    pub attachment_count: u8,
566    pub recipient_hash: [u8; 32],
567    pub ephemeral_pubkey: [u8; 32],
568}
569
570impl MessageHeader {
571    /// Parse header from bytes
572    pub fn from_bytes(data: &[u8]) -> Option<Self> {
573        if data.len() < SRC201_HEADER_SIZE {
574            return None;
575        }
576
577        let mut magic = [0u8; 4];
578        magic.copy_from_slice(&data[0..4]);
579
580        if magic != SRC201_MAGIC {
581            return None;
582        }
583
584        let version = data[4];
585        let flags = MessageFlags(data[5]);
586        let content_type = ContentType::from_byte(data[6])?;
587        let attachment_count = data[7];
588
589        let mut recipient_hash = [0u8; 32];
590        recipient_hash.copy_from_slice(&data[8..40]);
591
592        let mut ephemeral_pubkey = [0u8; 32];
593        ephemeral_pubkey.copy_from_slice(&data[40..72]);
594
595        Some(Self {
596            magic,
597            version,
598            flags,
599            content_type,
600            attachment_count,
601            recipient_hash,
602            ephemeral_pubkey,
603        })
604    }
605
606    /// Serialize header to bytes (used as AAD in AEAD)
607    pub fn to_bytes(&self) -> [u8; SRC201_HEADER_SIZE] {
608        let mut bytes = [0u8; SRC201_HEADER_SIZE];
609        bytes[0..4].copy_from_slice(&self.magic);
610        bytes[4] = self.version;
611        bytes[5] = self.flags.0;
612        bytes[6] = self.content_type as u8;
613        bytes[7] = self.attachment_count;
614        bytes[8..40].copy_from_slice(&self.recipient_hash);
615        bytes[40..72].copy_from_slice(&self.ephemeral_pubkey);
616        bytes
617    }
618}
619
620/// Validate an SRC-201 message format (basic checks)
621pub fn validate_message_format(data: &[u8]) -> Result<MessageHeader, &'static str> {
622    if data.len() < SRC201_HEADER_SIZE + SRC201_NONCE_SIZE + 2 + SRC201_TAG_SIZE {
623        return Err("Message too short");
624    }
625
626    let header = MessageHeader::from_bytes(data).ok_or("Invalid header")?;
627
628    if header.version != SRC201_VERSION {
629        return Err("Unsupported version");
630    }
631
632    // Check payload length field
633    let payload_len =
634        u16::from_be_bytes([data[SRC201_HEADER_SIZE + 24], data[SRC201_HEADER_SIZE + 25]]) as usize;
635
636    let expected_min_size = SRC201_HEADER_SIZE + SRC201_NONCE_SIZE + 2 + payload_len + SRC201_TAG_SIZE;
637    if data.len() < expected_min_size {
638        return Err("Payload length mismatch");
639    }
640
641    Ok(header)
642}
643
644#[cfg(test)]
645mod tests {
646    use super::*;
647
648    #[test]
649    fn test_message_flags() {
650        let mut flags = MessageFlags::new();
651        assert!(!flags.is_encrypted());
652
653        flags.set(MessageFlags::ENCRYPTED);
654        assert!(flags.is_encrypted());
655
656        flags.set(MessageFlags::HAS_REPLY_TO);
657        assert!(flags.has_reply_to());
658
659        flags.clear(MessageFlags::ENCRYPTED);
660        assert!(!flags.is_encrypted());
661        assert!(flags.has_reply_to());
662    }
663
664    #[test]
665    fn test_messaging_operation_from_byte() {
666        assert_eq!(
667            MessagingOperation::from_byte(0),
668            Some(MessagingOperation::SendMessage)
669        );
670        assert_eq!(
671            MessagingOperation::from_byte(128),
672            Some(MessagingOperation::SetDailyQuota)
673        );
674        assert!(MessagingOperation::from_byte(200).is_none());
675    }
676
677    #[test]
678    fn test_content_type() {
679        assert!(ContentType::TextPlain.is_text());
680        assert!(ContentType::ImagePng.is_image());
681        assert!(!ContentType::ApplicationJson.is_text());
682    }
683
684    #[test]
685    fn test_message_header_roundtrip() {
686        let header = MessageHeader {
687            magic: SRC201_MAGIC,
688            version: SRC201_VERSION,
689            flags: MessageFlags::encrypted(),
690            content_type: ContentType::TextPlain,
691            attachment_count: 0,
692            recipient_hash: [1u8; 32],
693            ephemeral_pubkey: [2u8; 32],
694        };
695
696        let bytes = header.to_bytes();
697        let parsed = MessageHeader::from_bytes(&bytes).unwrap();
698
699        assert_eq!(header, parsed);
700    }
701}