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