Skip to main content

p2panda_core/
operation.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Core p2panda data type offering distributed, secure and efficient data transfer between peers.
4//!
5//! Operations are used to carry any data from one peer to another (distributed), while assuming no
6//! reliable network connection (offline-first) and untrusted machines (cryptographically secure).
7//! The author of an operation uses it's [`SigningKey`] to cryptographically sign every operation.
8//! This can be verified and used for authentication by any other peer.
9//!
10//! Every operation consists of a [`Header`] and an optional [`Body`]. The body holds arbitrary
11//! bytes (up to the application to decide what should be inside). The header is used to
12//! cryptographically secure & authenticate the body and for providing ordered collections of
13//! operations when required.
14//!
15//! Operations have a `backlink` and `seq_num` field in the header. These are used to form a linked
16//! list of operations, where every subsequent operation points to the previous one by referencing
17//! its cryptographically secured hash.
18//!
19//! [Header extensions](crate::extensions) can be used to add additional information, like
20//! "pruning" points for removing old or unwanted data, "tombstones" for explicit deletion,
21//! capabilities or group encryption schemes or custom application-related features etc.
22//!
23//! Operations are encoded in CBOR format and use Ed25519 key pairs for digital signatures and
24//! BLAKE3 for hashing.
25//!
26//! ## Examples
27//!
28//! ### Construct and sign a header
29//!
30//! ```
31//! use p2panda_core::{Body, Header, SigningKey};
32//!
33//! let signing_key = SigningKey::generate();
34//!
35//! let body = Body::new("Hello, Sloth!".as_bytes());
36//! let mut header = Header {
37//!     version: 1,
38//!     verifying_key: signing_key.verifying_key(),
39//!     signature: None,
40//!     payload_size: body.size(),
41//!     payload_hash: Some(body.hash()),
42//!     seq_num: 0,
43//!     backlink: None,
44//!     extensions: (),
45//! };
46//!
47//! header.sign(&signing_key);
48//! ```
49//!
50//! ### Custom extensions
51//!
52//! ```
53//! use p2panda_core::{Body, Extension, Header, SigningKey, PruneFlag};
54//! use serde::{Serialize, Deserialize};
55//!
56//! let signing_key = SigningKey::generate();
57//!
58//! #[derive(Clone, Debug, Default, Serialize, Deserialize)]
59//! struct CustomExtensions {
60//!     prune_flag: PruneFlag,
61//! }
62//!
63//! impl Extension<PruneFlag> for CustomExtensions {
64//!     fn extract(header: &Header<Self>) -> Option<PruneFlag> {
65//!         Some(header.extensions.prune_flag.clone())
66//!     }
67//! }
68//!
69//! let extensions = CustomExtensions {
70//!     prune_flag: PruneFlag::new(true),
71//! };
72//!
73//! let body = Body::new("Prune from here please!".as_bytes());
74//! let mut header = Header {
75//!     version: 1,
76//!     verifying_key: signing_key.verifying_key(),
77//!     signature: None,
78//!     payload_size: body.size(),
79//!     payload_hash: Some(body.hash()),
80//!     seq_num: 0,
81//!     backlink: None,
82//!     extensions,
83//! };
84//!
85//! header.sign(&signing_key);
86//!
87//! let prune_flag: PruneFlag = header.extension().unwrap();
88//! assert!(prune_flag.is_set())
89//! ```
90use std::borrow::Borrow;
91
92use thiserror::Error;
93
94use crate::cbor::{DecodeError, decode_cbor, encode_cbor};
95use crate::extensions::{Extension, Extensions};
96use crate::hash::Hash;
97use crate::identity::{Signature, SigningKey, VerifyingKey};
98use crate::logs::SeqNum;
99use crate::traits::{Digest, Provenance};
100
101/// Encoded bytes of an operation header and optional body.
102pub type RawOperation = (Vec<u8>, Option<Vec<u8>>);
103
104/// Combined [`Header`], [`Body`] and operation [`struct@Hash`] (Operation Id).
105#[derive(Clone, Debug)]
106pub struct Operation<E = ()> {
107    pub hash: Hash,
108    pub header: Header<E>,
109    pub body: Option<Body>,
110}
111
112impl<E> Operation<E>
113where
114    E: Extensions,
115{
116    /// Get a reference to the operation header.
117    pub fn header(&self) -> &Header<E> {
118        &self.header
119    }
120
121    /// Get a reference to the operation body.
122    pub fn body(&self) -> Option<&Body> {
123        self.body.as_ref()
124    }
125}
126
127impl<E> PartialEq for Operation<E> {
128    fn eq(&self, other: &Self) -> bool {
129        self.hash.eq(&other.hash)
130    }
131}
132
133impl<E> Eq for Operation<E> {}
134
135impl<E> Borrow<Header<E>> for Operation<E> {
136    fn borrow(&self) -> &Header<E> {
137        &self.header
138    }
139}
140
141#[allow(clippy::non_canonical_partial_ord_impl)]
142impl<E> PartialOrd for Operation<E> {
143    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
144        Some(self.hash.cmp(&other.hash))
145    }
146}
147
148impl<E> Ord for Operation<E> {
149    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
150        self.hash.cmp(&other.hash)
151    }
152}
153
154impl<E> Digest<Hash> for Operation<E> {
155    fn hash(&self) -> Hash {
156        self.hash
157    }
158}
159
160impl<E> Provenance<VerifyingKey> for Operation<E>
161where
162    E: Extensions,
163{
164    fn author(&self) -> VerifyingKey {
165        self.header.verifying_key
166    }
167
168    fn verify(&self) -> bool {
169        self.header.verify()
170    }
171}
172
173pub type Version = u16;
174
175/// Header of a p2panda operation.
176///
177/// The header holds all metadata required to cryptographically secure and authenticate a message
178/// [`Body`] and, if required, apply ordering to collections of messages from the same or many
179/// authors.
180///
181/// ## Example
182///
183/// ```
184/// use p2panda_core::{Body, Header, Operation, SigningKey};
185///
186/// let signing_key = SigningKey::generate();
187///
188/// let body = Body::new("Hello, Sloth!".as_bytes());
189/// let mut header = Header {
190///     version: 1,
191///     verifying_key: signing_key.verifying_key(),
192///     signature: None,
193///     payload_size: body.size(),
194///     payload_hash: Some(body.hash()),
195///     seq_num: 0,
196///     backlink: None,
197///     extensions: (),
198/// };
199///
200/// // Sign the header with the author's private key.
201/// header.sign(&signing_key);
202/// ```
203#[derive(Clone, Debug, PartialEq, Eq)]
204#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
205pub struct Header<E = ()> {
206    /// Operation format version, allowing backwards compatibility when specification changes.
207    pub version: Version,
208
209    /// Author of this operation.
210    pub verifying_key: VerifyingKey,
211
212    /// Signature by author over all fields in header, providing authenticity.
213    pub signature: Option<Signature>,
214
215    /// Number of bytes of the body of this operation, must be zero if no body is given.
216    pub payload_size: u32,
217
218    /// Hash of the body of this operation, must be included if payload_size is non-zero and
219    /// omitted otherwise.
220    ///
221    /// Keeping the hash here allows us to delete the payload (off-chain data) while retaining the
222    /// ability to check the signature of the header.
223    pub payload_hash: Option<Hash>,
224
225    /// Number of operations this author has published to this log, begins with 0 and is always
226    /// incremented by 1 with each new operation by the same author.
227    pub seq_num: SeqNum,
228
229    /// Hash of the previous operation of the same author and log. Can be omitted if first
230    /// operation in log.
231    pub backlink: Option<Hash>,
232
233    /// Custom additional data.
234    //
235    // NOTE: If `E` is a Zero-Sized Type (ZST) we use unsafe code to skip the redundant field when
236    // encoding or decoding the header. See `zero_sized_extensions` for safety details.
237    //
238    // This allows us to keep the usage of Header ergonomic while assuring operations are encoded
239    // most efficiently and correctly according to p2panda's specification.
240    //
241    // An alternative would be to make this field an `Option` or introduce `E: Default` bounds to
242    // allow initialisation in safe code which both are annoying to deal with.
243    pub extensions: E,
244}
245
246impl<E: Default> Default for Header<E> {
247    fn default() -> Self {
248        Self {
249            version: 1,
250            verifying_key: VerifyingKey::default(),
251            signature: None,
252            payload_size: 0,
253            payload_hash: None,
254            seq_num: 0,
255            backlink: None,
256            extensions: E::default(),
257        }
258    }
259}
260
261impl<E> Header<E>
262where
263    E: Extensions,
264{
265    /// Header encoded to bytes in CBOR format.
266    pub fn to_bytes(&self) -> Vec<u8> {
267        encode_cbor(self)
268            // We can be sure that all values in this module are serializable and _if_ ciborium
269            // still fails then because of something really bad ..
270            .expect("CBOR encoder failed due to an critical IO error")
271    }
272
273    /// Add a signature to the header using the provided `SigningKey`.
274    ///
275    /// This method signs the byte representation of a header with any existing signature removed
276    /// before adding back the newly generated signature.
277    pub fn sign(&mut self, signing_key: &SigningKey) {
278        // Make sure the signature is not already set before we encode
279        self.signature = None;
280
281        let bytes = self.to_bytes();
282        self.signature = Some(signing_key.sign(&bytes));
283    }
284
285    /// Verify that the signature contained in this `Header` was generated by the claimed
286    /// public key.
287    pub fn verify(&self) -> bool {
288        match self.signature {
289            Some(claimed_signature) => {
290                let mut unsigned_header = self.clone();
291                unsigned_header.signature = None;
292                let unsigned_bytes = unsigned_header.to_bytes();
293                self.verifying_key
294                    .verify(&unsigned_bytes, &claimed_signature)
295            }
296            None => false,
297        }
298    }
299
300    /// BLAKE3 hash of the header bytes.
301    ///
302    /// This hash is used as the unique identifier of an operation, aka the Operation Id.
303    pub fn hash(&self) -> Hash {
304        Hash::digest(self.to_bytes())
305    }
306
307    /// Extract an extension value from the header.
308    pub fn extension<T>(&self) -> Option<T>
309    where
310        E: Extension<T>,
311    {
312        E::extract(self)
313    }
314}
315
316impl<E> Header<E> {
317    pub(crate) const fn has_non_zero_sized_extensions() -> bool {
318        std::mem::size_of::<E>() > 0
319    }
320
321    pub(crate) fn zero_sized_extensions() -> E {
322        assert!(!Self::has_non_zero_sized_extensions());
323
324        // SAFETY: The assertion guarantees E is a zero-sized type.
325        //
326        // For ZSTs, there are no bytes to initialize. std::mem::zeroed() on a ZST is a compile-time
327        // no-op with no actual memory operations.
328        unsafe { std::mem::zeroed() }
329    }
330
331    /// Number of fields included in the header.
332    ///
333    /// Fields instantiated with `None` values are excluded from the count.
334    pub(crate) fn field_count(&self) -> usize {
335        // There will always be a minimum of 4 fields in an unsigned header.
336        let mut count = 4;
337
338        if self.signature.is_some() {
339            count += 1;
340        }
341
342        if self.payload_hash.is_some() {
343            count += 1;
344        }
345
346        if self.backlink.is_some() {
347            count += 1;
348        }
349
350        if Self::has_non_zero_sized_extensions() {
351            count += 1;
352        }
353
354        count
355    }
356}
357
358#[cfg(any(test, feature = "test_utils"))]
359impl<E> Header<E>
360where
361    E: Extensions,
362{
363    pub fn to_hex(&self) -> String {
364        hex::encode(self.to_bytes())
365    }
366}
367
368impl<E> Provenance<VerifyingKey> for Header<E>
369where
370    E: Extensions,
371{
372    fn author(&self) -> VerifyingKey {
373        self.verifying_key
374    }
375
376    fn verify(&self) -> bool {
377        self.verify()
378    }
379}
380
381impl TryFrom<&[u8]> for Header {
382    type Error = DecodeError;
383
384    fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
385        decode_cbor(value)
386    }
387}
388
389/// Body of a p2panda operation containing arbitrary bytes.
390#[derive(Clone, Debug, PartialEq)]
391pub struct Body(pub(super) Vec<u8>);
392
393impl Body {
394    /// Construct a body from a byte slice.
395    pub fn new(bytes: &[u8]) -> Self {
396        Self(bytes.to_vec())
397    }
398
399    /// Access the underlying body bytes.
400    pub fn to_bytes(&self) -> Vec<u8> {
401        self.0.clone()
402    }
403
404    pub fn as_bytes(&self) -> &[u8] {
405        &self.0
406    }
407
408    /// BLAKE3 hash of the body bytes.
409    pub fn hash(&self) -> Hash {
410        Hash::digest(&self.0)
411    }
412
413    /// Size of body bytes.
414    pub fn size(&self) -> u32 {
415        self.0.len() as u32
416    }
417}
418
419#[cfg(any(test, feature = "test_utils"))]
420impl Body {
421    pub fn to_hex(&self) -> String {
422        hex::encode(self.as_bytes())
423    }
424}
425
426impl From<&[u8]> for Body {
427    fn from(value: &[u8]) -> Self {
428        Body::new(value)
429    }
430}
431
432impl From<Vec<u8>> for Body {
433    fn from(value: Vec<u8>) -> Self {
434        Body(value)
435    }
436}
437
438#[derive(Clone, Debug, Error)]
439pub enum OperationError {
440    #[error("operation version {0} is not supported, needs to be <= {1}")]
441    UnsupportedVersion(Version, Version),
442
443    #[error("operation needs to be signed")]
444    MissingSignature,
445
446    #[error("signature does not match claimed public key")]
447    SignatureMismatch,
448
449    #[error("sequence number can't be 0 when backlink is given")]
450    SeqNumMismatch,
451
452    #[error("payload hash and -size need to be defined together")]
453    InconsistentPayloadInfo,
454
455    #[error("needs payload hash in header when body is given")]
456    MissingPayloadHash,
457
458    #[error("payload hash and size do not match given body")]
459    PayloadMismatch,
460
461    #[error("logs can not contain operations of different authors")]
462    TooManyAuthors,
463
464    #[error("expected sequence number {0} but found {1}")]
465    SeqNumNonIncremental(SeqNum, SeqNum),
466
467    #[error("expected backlink but none was given")]
468    BacklinkMissing,
469
470    #[error("given backlink did not match previous operation")]
471    BacklinkMismatch,
472}
473
474/// Validate the header and body (when provided) of a single operation. All basic header
475/// validation is performed (identical to [`validate_header`]()) and additionally the body bytes
476/// hash and size are checked to be correct.
477///
478/// This method validates that the following conditions are true:
479/// * Signature can be verified against the author public key and unsigned header bytes
480/// * Header version is supported (currently only version 1 is supported)
481/// * If `payload_hash` is set the `payload_size` is > `0` otherwise it is zero
482/// * If `backlink` is set then `seq_num` is > `0` otherwise it is zero
483/// * If provided the body bytes hash and size match those claimed in the header
484pub fn validate_operation<E>(operation: impl Borrow<Operation<E>>) -> Result<(), OperationError>
485where
486    E: Extensions,
487{
488    let operation = operation.borrow();
489    validate_header(&operation.header)?;
490
491    let claimed_payload_size = operation.header.payload_size;
492    let claimed_payload_hash: Option<Hash> = match claimed_payload_size {
493        0 => None,
494        _ => {
495            let hash = operation
496                .header
497                .payload_hash
498                .ok_or(OperationError::MissingPayloadHash)?;
499            Some(hash)
500        }
501    };
502
503    if let Some(body) = &operation.body
504        && (claimed_payload_hash != Some(body.hash()) || claimed_payload_size != body.size())
505    {
506        return Err(OperationError::PayloadMismatch);
507    }
508
509    Ok(())
510}
511
512/// Validate an operation header.
513///
514/// This method validates that the following conditions are true:
515/// * Signature can be verified against the author public key and unsigned header bytes
516/// * Header version is supported (currently only version 1 is supported)
517/// * If `payload_hash` is set the `payload_size` is > `0` otherwise it is zero
518/// * If `backlink` is set then `seq_num` is > `0` otherwise it is zero
519pub fn validate_header<E>(header: &Header<E>) -> Result<(), OperationError>
520where
521    E: Extensions,
522{
523    if !header.verify() {
524        return Err(OperationError::SignatureMismatch);
525    }
526
527    if header.version != 1 {
528        return Err(OperationError::UnsupportedVersion(header.version, 1));
529    }
530
531    if (header.payload_hash.is_some() && header.payload_size == 0)
532        || (header.payload_hash.is_none() && header.payload_size > 0)
533    {
534        return Err(OperationError::InconsistentPayloadInfo);
535    }
536
537    if header.backlink.is_some() && header.seq_num == 0 {
538        return Err(OperationError::SeqNumMismatch);
539    }
540
541    if header.backlink.is_none() && header.seq_num > 0 {
542        return Err(OperationError::BacklinkMissing);
543    }
544
545    Ok(())
546}
547
548/// Validate a backlink contained in a header against a past header which is assumed to have been
549/// retrieved from a local store.
550///
551/// This method validates that the following conditions are true:
552/// * Current and past headers contain the same public key
553/// * Current headers seq number increments from the past one by exactly `1`
554/// * Backlink hash contained in the current header matches the hash of the past header
555pub fn validate_backlink<E>(
556    past_header: impl Borrow<Header<E>>,
557    header: impl Borrow<Header<E>>,
558) -> Result<(), OperationError>
559where
560    E: Extensions,
561{
562    let past_header = past_header.borrow();
563    let header = header.borrow();
564
565    if past_header.verifying_key != header.verifying_key {
566        return Err(OperationError::TooManyAuthors);
567    }
568
569    if past_header.seq_num + 1 != header.seq_num {
570        return Err(OperationError::SeqNumNonIncremental(
571            past_header.seq_num + 1,
572            header.seq_num,
573        ));
574    }
575
576    match header.backlink {
577        Some(backlink) => {
578            if past_header.hash() != backlink {
579                return Err(OperationError::BacklinkMismatch);
580            }
581        }
582        None => {
583            return Err(OperationError::BacklinkMissing);
584        }
585    }
586
587    Ok(())
588}
589
590#[cfg(test)]
591mod tests {
592    use serde::{Deserialize, Serialize};
593
594    use crate::{Extension, SigningKey};
595
596    use super::*;
597
598    #[test]
599    fn simple_extension_type_parameter() {
600        let signing_key = SigningKey::generate();
601        let body = Body::new("Hello, Sloth!".as_bytes());
602        let mut header = Header {
603            version: 1,
604            verifying_key: signing_key.verifying_key(),
605            signature: None,
606            payload_size: body.size(),
607            payload_hash: Some(body.hash()),
608            seq_num: 0,
609            backlink: None,
610            extensions: (),
611        };
612
613        header.sign(&signing_key);
614    }
615
616    #[test]
617    fn sign_and_verify() {
618        let signing_key = SigningKey::generate();
619        let body = Body::new("Hello, Sloth!".as_bytes());
620        type CustomExtensions = ();
621
622        let mut header = Header {
623            version: 1,
624            verifying_key: signing_key.verifying_key(),
625            signature: None,
626            payload_size: body.size(),
627            payload_hash: Some(body.hash()),
628            seq_num: 0,
629            backlink: None,
630            extensions: None::<CustomExtensions>,
631        };
632        assert!(!header.verify());
633
634        header.sign(&signing_key);
635        assert!(header.verify());
636
637        let operation = Operation {
638            hash: header.hash(),
639            header,
640            body: Some(body),
641        };
642        assert!(validate_operation(&operation).is_ok());
643    }
644
645    #[test]
646    fn valid_backlink_header() {
647        let signing_key = SigningKey::generate();
648
649        let mut header_0 = Header::<()> {
650            version: 1,
651            verifying_key: signing_key.verifying_key(),
652            signature: None,
653            payload_size: 0,
654            payload_hash: None,
655            seq_num: 0,
656            backlink: None,
657            extensions: (),
658        };
659        header_0.sign(&signing_key);
660        assert!(validate_header(&header_0).is_ok());
661
662        let mut header_1 = Header::<()> {
663            version: 1,
664            verifying_key: signing_key.verifying_key(),
665            signature: None,
666            payload_size: 0,
667            payload_hash: None,
668            seq_num: 1,
669            backlink: Some(header_0.hash()),
670            extensions: (),
671        };
672        header_1.sign(&signing_key);
673        assert!(validate_header(&header_1).is_ok());
674
675        assert!(validate_backlink(&header_0, &header_1).is_ok());
676    }
677
678    #[test]
679    fn invalid_operations() {
680        let signing_key = SigningKey::generate();
681        let body: Body = Body::new("Hello, Sloth!".as_bytes());
682
683        let header_base = Header::<()> {
684            version: 1,
685            verifying_key: signing_key.verifying_key(),
686            signature: None,
687            payload_size: body.size(),
688            payload_hash: Some(body.hash()),
689            seq_num: 0,
690            backlink: None,
691            extensions: (),
692        };
693
694        // Incompatible operation format
695        let mut header = header_base.clone();
696        header.version = 0;
697        header.sign(&signing_key);
698        std::assert_matches!(
699            validate_header(&header),
700            Err(OperationError::UnsupportedVersion(0, 1))
701        );
702
703        // Signature doesn't match public key
704        let mut header = header_base.clone();
705        header.verifying_key = SigningKey::generate().verifying_key();
706        header.sign(&signing_key);
707        std::assert_matches!(
708            validate_header(&header),
709            Err(OperationError::SignatureMismatch)
710        );
711
712        // Backlink missing
713        let mut header = header_base.clone();
714        header.seq_num = 1;
715        header.sign(&signing_key);
716        std::assert_matches!(
717            validate_header(&header),
718            Err(OperationError::BacklinkMissing)
719        );
720
721        // Backlink given but sequence number indicates none
722        let mut header = header_base.clone();
723        header.backlink = Some(Hash::digest(vec![4, 5, 6]));
724        header.sign(&signing_key);
725        std::assert_matches!(
726            validate_header(&header),
727            Err(OperationError::SeqNumMismatch)
728        );
729
730        // Payload size does not match
731        let mut header = header_base.clone();
732        header.payload_size = 11;
733        header.sign(&signing_key);
734        std::assert_matches!(
735            validate_operation(&Operation {
736                hash: header.hash(),
737                header,
738                body: Some(body.clone()),
739            }),
740            Err(OperationError::PayloadMismatch)
741        );
742
743        // Payload hash does not match
744        let mut header = header_base.clone();
745        header.payload_hash = Some(Hash::digest(vec![4, 5, 6]));
746        header.sign(&signing_key);
747        std::assert_matches!(
748            validate_operation(&Operation {
749                hash: header.hash(),
750                header,
751                body: Some(body.clone()),
752            }),
753            Err(OperationError::PayloadMismatch)
754        );
755    }
756
757    #[test]
758    fn extensions() {
759        #[derive(Clone, Debug, Serialize, Deserialize)]
760        struct LogId(Hash);
761
762        #[derive(Clone, Debug, Serialize, Deserialize)]
763        struct Expiry(u64);
764
765        #[derive(Clone, Debug, Serialize, Deserialize)]
766        struct CustomExtensions {
767            log_id: Option<LogId>,
768            expires: Expiry,
769        }
770
771        impl Extension<LogId> for CustomExtensions {
772            fn extract(header: &Header<Self>) -> Option<LogId> {
773                if header.seq_num == 0 {
774                    return Some(LogId(header.hash()));
775                };
776
777                header.extensions.log_id.clone()
778            }
779        }
780
781        impl Extension<Expiry> for CustomExtensions {
782            fn extract(header: &Header<Self>) -> Option<Expiry> {
783                Some(header.extensions.expires.clone())
784            }
785        }
786
787        let extensions = CustomExtensions {
788            log_id: None,
789            expires: Expiry(0123456),
790        };
791
792        let signing_key = SigningKey::generate();
793        let body: Body = Body::new("Hello, Sloth!".as_bytes());
794
795        let mut header = Header {
796            version: 1,
797            verifying_key: signing_key.verifying_key(),
798            signature: None,
799            payload_size: body.size(),
800            payload_hash: Some(body.hash()),
801            seq_num: 0,
802            backlink: None,
803            extensions: extensions.clone(),
804        };
805
806        header.sign(&signing_key);
807
808        // Thanks to blanket implementation of Extension<T> on Header we can extract the extension
809        // value from the header itself.
810        let log_id: LogId = header.extension().unwrap();
811        let expiry: Expiry = header.extension().unwrap();
812
813        assert_eq!(header.hash(), log_id.0);
814        assert_eq!(extensions.expires.0, expiry.0);
815    }
816
817    #[test]
818    fn zst_size_matches_mem_checks() {
819        struct ZstExtensions;
820        assert_eq!(std::mem::size_of::<ZstExtensions>(), 0);
821        assert!(!Header::<ZstExtensions>::has_non_zero_sized_extensions());
822
823        #[allow(unused)]
824        struct NonZstExtensions(u32);
825        assert_ne!(std::mem::size_of::<NonZstExtensions>(), 0);
826        assert!(Header::<NonZstExtensions>::has_non_zero_sized_extensions());
827    }
828}