p2panda_core/
operation.rs

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