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