1use thiserror::Error;
97
98use crate::cbor::{DecodeError, decode_cbor, encode_cbor};
99use crate::hash::Hash;
100use crate::identity::{PrivateKey, PublicKey, Signature};
101use crate::{Extension, Extensions};
102
103pub type RawOperation = (Vec<u8>, Option<Vec<u8>>);
105
106#[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
122#[allow(clippy::non_canonical_partial_ord_impl)]
123impl<E> PartialOrd for Operation<E> {
124 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
125 Some(self.hash.cmp(&other.hash))
126 }
127}
128
129impl<E> Ord for Operation<E> {
130 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
131 self.hash.cmp(&other.hash)
132 }
133}
134
135#[derive(Clone, Debug, PartialEq)]
166#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
167pub struct Header<E = ()> {
168 pub version: u64,
170
171 pub public_key: PublicKey,
173
174 pub signature: Option<Signature>,
176
177 pub payload_size: u64,
179
180 pub payload_hash: Option<Hash>,
186
187 pub timestamp: u64,
189
190 pub seq_num: u64,
193
194 pub backlink: Option<Hash>,
197
198 pub previous: Vec<Hash>,
202
203 pub extensions: E,
205}
206
207impl<E: Default> Default for Header<E> {
208 fn default() -> Self {
209 Self {
210 version: 1,
211 public_key: PublicKey::default(),
212 signature: None,
213 payload_size: 0,
214 payload_hash: None,
215 timestamp: 0,
216 seq_num: 0,
217 backlink: None,
218 previous: vec![],
219 extensions: E::default(),
220 }
221 }
222}
223
224impl<E> Header<E>
225where
226 E: Extensions,
227{
228 pub fn to_bytes(&self) -> Vec<u8> {
230 encode_cbor(self)
231 .expect("CBOR encoder failed due to an critical IO error")
234 }
235
236 pub fn sign(&mut self, private_key: &PrivateKey) {
241 self.signature = None;
243
244 let bytes = self.to_bytes();
245 self.signature = Some(private_key.sign(&bytes));
246 }
247
248 pub fn verify(&self) -> bool {
251 match self.signature {
252 Some(claimed_signature) => {
253 let mut unsigned_header = self.clone();
254 unsigned_header.signature = None;
255 let unsigned_bytes = unsigned_header.to_bytes();
256 self.public_key.verify(&unsigned_bytes, &claimed_signature)
257 }
258 None => false,
259 }
260 }
261
262 pub fn hash(&self) -> Hash {
266 Hash::new(self.to_bytes())
267 }
268
269 pub fn extension<T>(&self) -> Option<T>
271 where
272 E: Extension<T>,
273 {
274 E::extract(self)
275 }
276}
277
278impl<E> Header<E> {
279 pub(crate) fn field_count(&self) -> usize {
283 let mut count = 7;
286
287 if self.signature.is_some() {
288 count += 1;
289 }
290
291 if self.payload_hash.is_some() {
292 count += 1;
293 }
294
295 if self.backlink.is_some() {
296 count += 1;
297 }
298
299 count
300 }
301}
302
303impl TryFrom<&[u8]> for Header {
304 type Error = DecodeError;
305
306 fn try_from(value: &[u8]) -> Result<Self, Self::Error> {
307 decode_cbor(value)
308 }
309}
310
311#[derive(Clone, Debug, PartialEq)]
313pub struct Body(pub(super) Vec<u8>);
314
315impl Body {
316 pub fn new(bytes: &[u8]) -> Self {
318 Self(bytes.to_vec())
319 }
320
321 pub fn to_bytes(&self) -> Vec<u8> {
323 self.0.clone()
324 }
325
326 pub fn hash(&self) -> Hash {
328 Hash::new(&self.0)
329 }
330
331 pub fn size(&self) -> u64 {
333 self.0.len() as u64
334 }
335}
336
337impl From<&[u8]> for Body {
338 fn from(value: &[u8]) -> Self {
339 Body::new(value)
340 }
341}
342
343impl From<Vec<u8>> for Body {
344 fn from(value: Vec<u8>) -> Self {
345 Body(value)
346 }
347}
348
349#[derive(Clone, Debug, Error)]
350pub enum OperationError {
351 #[error("operation version {0} is not supported, needs to be <= {1}")]
352 UnsupportedVersion(u64, u64),
353
354 #[error("operation needs to be signed")]
355 MissingSignature,
356
357 #[error("signature does not match claimed public key")]
358 SignatureMismatch,
359
360 #[error("sequence number can't be 0 when backlink is given")]
361 SeqNumMismatch,
362
363 #[error("payload hash and -size need to be defined together")]
364 InconsistentPayloadInfo,
365
366 #[error("needs payload hash in header when body is given")]
367 MissingPayloadHash,
368
369 #[error("payload hash and size do not match given body")]
370 PayloadMismatch,
371
372 #[error("logs can not contain operations of different authors")]
373 TooManyAuthors,
374
375 #[error("expected sequence number {0} but found {1}")]
376 SeqNumNonIncremental(u64, u64),
377
378 #[error("expected backlink but none was given")]
379 BacklinkMissing,
380
381 #[error("given backlink did not match previous operation")]
382 BacklinkMismatch,
383}
384
385pub fn validate_operation<E>(operation: &Operation<E>) -> Result<(), OperationError>
396where
397 E: Extensions,
398{
399 validate_header(&operation.header)?;
400
401 let claimed_payload_size = operation.header.payload_size;
402 let claimed_payload_hash: Option<Hash> = match claimed_payload_size {
403 0 => None,
404 _ => {
405 let hash = operation
406 .header
407 .payload_hash
408 .ok_or(OperationError::MissingPayloadHash)?;
409 Some(hash)
410 }
411 };
412
413 if let Some(body) = &operation.body
414 && (claimed_payload_hash != Some(body.hash()) || claimed_payload_size != body.size())
415 {
416 return Err(OperationError::PayloadMismatch);
417 }
418
419 Ok(())
420}
421
422pub fn validate_header<E>(header: &Header<E>) -> Result<(), OperationError>
430where
431 E: Extensions,
432{
433 if !header.verify() {
434 return Err(OperationError::SignatureMismatch);
435 }
436
437 if header.version != 1 {
438 return Err(OperationError::UnsupportedVersion(header.version, 1));
439 }
440
441 if (header.payload_hash.is_some() && header.payload_size == 0)
442 || (header.payload_hash.is_none() && header.payload_size > 0)
443 {
444 return Err(OperationError::InconsistentPayloadInfo);
445 }
446
447 if header.backlink.is_some() && header.seq_num == 0 {
448 return Err(OperationError::SeqNumMismatch);
449 }
450
451 if header.backlink.is_none() && header.seq_num > 0 {
452 return Err(OperationError::BacklinkMissing);
453 }
454
455 Ok(())
456}
457
458pub fn validate_backlink<E>(
466 past_header: &Header<E>,
467 header: &Header<E>,
468) -> Result<(), OperationError>
469where
470 E: Extensions,
471{
472 if past_header.public_key != header.public_key {
473 return Err(OperationError::TooManyAuthors);
474 }
475
476 if past_header.seq_num + 1 != header.seq_num {
477 return Err(OperationError::SeqNumNonIncremental(
478 past_header.seq_num + 1,
479 header.seq_num,
480 ));
481 }
482
483 match header.backlink {
484 Some(backlink) => {
485 if past_header.hash() != backlink {
486 return Err(OperationError::BacklinkMismatch);
487 }
488 }
489 None => {
490 return Err(OperationError::BacklinkMissing);
491 }
492 }
493
494 Ok(())
495}
496
497#[cfg(test)]
498mod tests {
499 use serde::{Deserialize, Serialize};
500
501 use crate::{Extension, PrivateKey};
502
503 use super::*;
504
505 #[test]
506 fn simple_extension_type_parameter() {
507 let private_key = PrivateKey::new();
508 let body = Body::new("Hello, Sloth!".as_bytes());
509 let mut header = Header {
510 version: 1,
511 public_key: private_key.public_key(),
512 signature: None,
513 payload_size: body.size(),
514 payload_hash: Some(body.hash()),
515 timestamp: 0,
516 seq_num: 0,
517 backlink: None,
518 previous: vec![],
519 extensions: (),
520 };
521
522 header.sign(&private_key);
523 }
524
525 #[test]
526 fn sign_and_verify() {
527 let private_key = PrivateKey::new();
528 let body = Body::new("Hello, Sloth!".as_bytes());
529 type CustomExtensions = ();
530
531 let mut header = Header {
532 version: 1,
533 public_key: private_key.public_key(),
534 signature: None,
535 payload_size: body.size(),
536 payload_hash: Some(body.hash()),
537 timestamp: 0,
538 seq_num: 0,
539 backlink: None,
540 previous: vec![],
541 extensions: None::<CustomExtensions>,
542 };
543 assert!(!header.verify());
544
545 header.sign(&private_key);
546 assert!(header.verify());
547
548 let operation = Operation {
549 hash: header.hash(),
550 header,
551 body: Some(body),
552 };
553 assert!(validate_operation(&operation).is_ok());
554 }
555
556 #[test]
557 fn valid_backlink_header() {
558 let private_key = PrivateKey::new();
559
560 let mut header_0 = Header::<()> {
561 version: 1,
562 public_key: private_key.public_key(),
563 signature: None,
564 payload_size: 0,
565 payload_hash: None,
566 timestamp: 0,
567 seq_num: 0,
568 backlink: None,
569 previous: vec![],
570 extensions: (),
571 };
572 header_0.sign(&private_key);
573 assert!(validate_header(&header_0).is_ok());
574
575 let mut header_1 = Header::<()> {
576 version: 1,
577 public_key: private_key.public_key(),
578 signature: None,
579 payload_size: 0,
580 payload_hash: None,
581 timestamp: 0,
582 seq_num: 1,
583 backlink: Some(header_0.hash()),
584 previous: vec![],
585 extensions: (),
586 };
587 header_1.sign(&private_key);
588 assert!(validate_header(&header_1).is_ok());
589
590 assert!(validate_backlink(&header_0, &header_1).is_ok());
591 }
592
593 #[test]
594 fn invalid_operations() {
595 let private_key = PrivateKey::new();
596 let body: Body = Body::new("Hello, Sloth!".as_bytes());
597
598 let header_base = Header::<()> {
599 version: 1,
600 public_key: private_key.public_key(),
601 signature: None,
602 payload_size: body.size(),
603 payload_hash: Some(body.hash()),
604 timestamp: 0,
605 seq_num: 0,
606 backlink: None,
607 previous: vec![],
608 extensions: (),
609 };
610
611 let mut header = header_base.clone();
613 header.version = 0;
614 header.sign(&private_key);
615 assert!(matches!(
616 validate_header(&header),
617 Err(OperationError::UnsupportedVersion(0, 1))
618 ));
619
620 let mut header = header_base.clone();
622 header.public_key = PrivateKey::new().public_key();
623 header.sign(&private_key);
624 assert!(matches!(
625 validate_header(&header),
626 Err(OperationError::SignatureMismatch)
627 ));
628
629 let mut header = header_base.clone();
631 header.seq_num = 1;
632 header.sign(&private_key);
633 assert!(matches!(
634 validate_header(&header),
635 Err(OperationError::BacklinkMissing)
636 ));
637
638 let mut header = header_base.clone();
640 header.backlink = Some(Hash::new(vec![4, 5, 6]));
641 header.sign(&private_key);
642 assert!(matches!(
643 validate_header(&header),
644 Err(OperationError::SeqNumMismatch)
645 ));
646
647 let mut header = header_base.clone();
649 header.payload_size = 11;
650 header.sign(&private_key);
651 assert!(matches!(
652 validate_operation(&Operation {
653 hash: header.hash(),
654 header,
655 body: Some(body.clone()),
656 }),
657 Err(OperationError::PayloadMismatch)
658 ));
659
660 let mut header = header_base.clone();
662 header.payload_hash = Some(Hash::new(vec![4, 5, 6]));
663 header.sign(&private_key);
664 assert!(matches!(
665 validate_operation(&Operation {
666 hash: header.hash(),
667 header,
668 body: Some(body.clone()),
669 }),
670 Err(OperationError::PayloadMismatch)
671 ));
672 }
673
674 #[test]
675 fn extensions() {
676 #[derive(Clone, Debug, Serialize, Deserialize)]
677 struct LogId(Hash);
678
679 #[derive(Clone, Debug, Serialize, Deserialize)]
680 struct Expiry(u64);
681
682 #[derive(Clone, Debug, Serialize, Deserialize)]
683 struct CustomExtensions {
684 log_id: Option<LogId>,
685 expires: Expiry,
686 }
687
688 impl Extension<LogId> for CustomExtensions {
689 fn extract(header: &Header<Self>) -> Option<LogId> {
690 if header.seq_num == 0 {
691 return Some(LogId(header.hash()));
692 };
693
694 header.extensions.log_id.clone()
695 }
696 }
697
698 impl Extension<Expiry> for CustomExtensions {
699 fn extract(header: &Header<Self>) -> Option<Expiry> {
700 Some(header.extensions.expires.clone())
701 }
702 }
703
704 let extensions = CustomExtensions {
705 log_id: None,
706 expires: Expiry(0123456),
707 };
708
709 let private_key = PrivateKey::new();
710 let body: Body = Body::new("Hello, Sloth!".as_bytes());
711
712 let mut header = Header {
713 version: 1,
714 public_key: private_key.public_key(),
715 signature: None,
716 payload_size: body.size(),
717 payload_hash: Some(body.hash()),
718 timestamp: 0,
719 seq_num: 0,
720 backlink: None,
721 previous: vec![],
722 extensions: extensions.clone(),
723 };
724
725 header.sign(&private_key);
726
727 let log_id: LogId = header.extension().unwrap();
730 let expiry: Expiry = header.extension().unwrap();
731
732 assert_eq!(header.hash(), log_id.0);
733 assert_eq!(extensions.expires.0, expiry.0);
734 }
735}