1use 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
107pub type RawOperation = (Vec<u8>, Option<Vec<u8>>);
109
110#[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#[derive(Clone, Debug, PartialEq)]
169#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
170pub struct Header<E = ()> {
171 pub version: u64,
173
174 pub public_key: PublicKey,
176
177 pub signature: Option<Signature>,
179
180 pub payload_size: u64,
182
183 pub payload_hash: Option<Hash>,
189
190 pub timestamp: u64,
192
193 pub seq_num: u64,
196
197 pub backlink: Option<Hash>,
200
201 pub previous: Vec<Hash>,
205
206 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 pub fn to_bytes(&self) -> Vec<u8> {
233 encode_cbor(self)
234 .expect("CBOR encoder failed due to an critical IO error")
237 }
238
239 pub fn sign(&mut self, private_key: &PrivateKey) {
244 self.signature = None;
246
247 let bytes = self.to_bytes();
248 self.signature = Some(private_key.sign(&bytes));
249 }
250
251 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 pub fn hash(&self) -> Hash {
269 Hash::new(self.to_bytes())
270 }
271
272 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 pub(crate) fn field_count(&self) -> usize {
286 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#[derive(Clone, Debug, PartialEq)]
319pub struct Body(pub(super) Vec<u8>);
320
321impl Body {
322 pub fn new(bytes: &[u8]) -> Self {
324 Self(bytes.to_vec())
325 }
326
327 pub fn to_bytes(&self) -> Vec<u8> {
329 self.0.clone()
330 }
331
332 pub fn hash(&self) -> Hash {
334 Hash::new(&self.0)
335 }
336
337 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
391pub 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
428pub 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
464pub 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 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 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 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 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 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 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 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}