1use 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
101pub type RawOperation = (Vec<u8>, Option<Vec<u8>>);
103
104#[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 pub fn header(&self) -> &Header<E> {
118 &self.header
119 }
120
121 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#[derive(Clone, Debug, PartialEq, Eq)]
204#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
205pub struct Header<E = ()> {
206 pub version: Version,
208
209 pub verifying_key: VerifyingKey,
211
212 pub signature: Option<Signature>,
214
215 pub payload_size: u32,
217
218 pub payload_hash: Option<Hash>,
224
225 pub seq_num: SeqNum,
228
229 pub backlink: Option<Hash>,
232
233 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 pub fn to_bytes(&self) -> Vec<u8> {
267 encode_cbor(self)
268 .expect("CBOR encoder failed due to an critical IO error")
271 }
272
273 pub fn sign(&mut self, signing_key: &SigningKey) {
278 self.signature = None;
280
281 let bytes = self.to_bytes();
282 self.signature = Some(signing_key.sign(&bytes));
283 }
284
285 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 pub fn hash(&self) -> Hash {
304 Hash::digest(self.to_bytes())
305 }
306
307 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 unsafe { std::mem::zeroed() }
329 }
330
331 pub(crate) fn field_count(&self) -> usize {
335 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#[derive(Clone, Debug, PartialEq)]
391pub struct Body(pub(super) Vec<u8>);
392
393impl Body {
394 pub fn new(bytes: &[u8]) -> Self {
396 Self(bytes.to_vec())
397 }
398
399 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 pub fn hash(&self) -> Hash {
410 Hash::digest(&self.0)
411 }
412
413 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
474pub 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
512pub 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
548pub 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 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 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 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 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 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 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 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}