1use chrono::{DateTime, Utc};
9use git2::{Commit, ErrorCode, Repository, Signature};
10
11use super::cache;
12use super::incremental::{self, IncrementalResult};
13use super::types::Prefix;
14use super::validate::validate_for_append;
15use super::{Event, IcpEvent, KeyState};
16use crate::domain::EventHash;
17use crate::witness::{event_hash_to_oid, oid_to_event_hash};
18
19#[derive(Debug, thiserror::Error)]
21#[non_exhaustive]
22pub enum KelError {
23 #[error("Git error: {0}")]
24 Git(#[from] git2::Error),
25
26 #[error("Serialization error: {0}")]
27 Serialization(String),
28
29 #[error("KEL not found for prefix: {0}")]
30 NotFound(String),
31
32 #[error("Invalid operation: {0}")]
33 InvalidOperation(String),
34
35 #[error("Invalid data: {0}")]
36 InvalidData(String),
37
38 #[error("Chain integrity error: {0}")]
39 ChainIntegrity(String),
40
41 #[error("Validation failed: {0}")]
42 ValidationFailed(#[from] super::ValidationError),
43}
44
45impl auths_core::error::AuthsErrorInfo for KelError {
46 fn error_code(&self) -> &'static str {
47 match self {
48 Self::Git(_) => "AUTHS-E4601",
49 Self::Serialization(_) => "AUTHS-E4602",
50 Self::NotFound(_) => "AUTHS-E4603",
51 Self::InvalidOperation(_) => "AUTHS-E4604",
52 Self::InvalidData(_) => "AUTHS-E4605",
53 Self::ChainIntegrity(_) => "AUTHS-E4606",
54 Self::ValidationFailed(_) => "AUTHS-E4607",
55 }
56 }
57
58 fn suggestion(&self) -> Option<&'static str> {
59 match self {
60 Self::Git(_) => Some("Check that the Git repository is accessible and not corrupted"),
61 Self::Serialization(_) => None,
62 Self::NotFound(_) => Some("Initialize the identity first with 'auths init'"),
63 Self::InvalidOperation(_) => None,
64 Self::InvalidData(_) => Some("The KEL data may be corrupted; try re-syncing"),
65 Self::ChainIntegrity(_) => {
66 Some("The KEL has non-linear history; this indicates tampering")
67 }
68 Self::ValidationFailed(_) => None,
69 }
70 }
71}
72
73const EVENT_BLOB_NAME: &str = "event.json";
75
76fn kel_ref(prefix: &Prefix) -> String {
78 format!("refs/did/keri/{}/kel", prefix.as_str())
79}
80
81pub struct GitKel<'a> {
92 repo: &'a Repository,
93 prefix: Prefix,
94 ref_path: String,
95}
96
97impl<'a> GitKel<'a> {
98 pub fn new(repo: &'a Repository, prefix: impl Into<String>) -> Self {
100 let prefix = Prefix::new_unchecked(prefix.into());
101 let ref_path = kel_ref(&prefix);
102 Self {
103 repo,
104 prefix,
105 ref_path,
106 }
107 }
108
109 pub fn with_ref(repo: &'a Repository, prefix: impl Into<String>, ref_path: String) -> Self {
123 Self {
124 repo,
125 prefix: Prefix::new_unchecked(prefix.into()),
126 ref_path,
127 }
128 }
129
130 pub fn prefix(&self) -> &Prefix {
132 &self.prefix
133 }
134
135 pub(crate) fn workdir(&self) -> &std::path::Path {
142 self.repo.workdir().unwrap_or_else(|| self.repo.path())
143 }
144
145 pub fn exists(&self) -> bool {
147 self.repo.find_reference(&self.ref_path).is_ok()
148 }
149
150 pub fn create(
155 &self,
156 event: &IcpEvent,
157 now: chrono::DateTime<chrono::Utc>,
158 ) -> Result<EventHash, KelError> {
159 if self.exists() {
160 return Err(KelError::InvalidOperation(format!(
161 "KEL already exists for prefix: {}",
162 self.prefix.as_str()
163 )));
164 }
165
166 let wrapped = Event::Icp(event.clone());
167 let json = serde_json::to_vec_pretty(&wrapped)
168 .map_err(|e| KelError::Serialization(e.to_string()))?;
169
170 let blob_oid = self.repo.blob(&json)?;
171 let mut tree_builder = self.repo.treebuilder(None)?;
172 tree_builder.insert(EVENT_BLOB_NAME, blob_oid, 0o100644)?;
173 let tree_oid = tree_builder.write()?;
174 let tree = self.repo.find_tree(tree_oid)?;
175
176 let sig = self.signature(now)?;
177 let commit_oid = self.repo.commit(
178 Some(&self.ref_path),
179 &sig,
180 &sig,
181 &format!("KERI inception: {}", event.i.as_str()),
182 &tree,
183 &[],
184 )?;
185
186 Ok(oid_to_event_hash(commit_oid))
187 }
188
189 pub fn append(
195 &self,
196 event: &Event,
197 now: chrono::DateTime<chrono::Utc>,
198 ) -> Result<EventHash, KelError> {
199 let ref_name = &self.ref_path;
200
201 let reference = self.repo.find_reference(ref_name).map_err(|e| {
202 if e.code() == ErrorCode::NotFound {
203 KelError::NotFound(self.prefix.as_str().to_string())
204 } else {
205 KelError::Git(e)
206 }
207 })?;
208 let parent_commit = reference.peel_to_commit()?;
209
210 let state = self.build_current_state()?;
212 validate_for_append(event, &state)?;
213
214 let json =
215 serde_json::to_vec_pretty(event).map_err(|e| KelError::Serialization(e.to_string()))?;
216
217 let blob_oid = self.repo.blob(&json)?;
218 let mut tree_builder = self.repo.treebuilder(None)?;
219 tree_builder.insert(EVENT_BLOB_NAME, blob_oid, 0o100644)?;
220 let tree_oid = tree_builder.write()?;
221 let tree = self.repo.find_tree(tree_oid)?;
222
223 let msg = match event {
224 Event::Rot(e) => format!("KERI rotation: s={}", e.s),
225 Event::Ixn(e) => format!("KERI interaction: s={}", e.s),
226 Event::Drt(e) => format!("KERI delegated rotation: s={}", e.s),
227 Event::Icp(_) | Event::Dip(_) => unreachable!(),
228 };
229
230 let sig = self.signature(now)?;
231 let commit_oid =
232 self.repo
233 .commit(Some(ref_name), &sig, &sig, &msg, &tree, &[&parent_commit])?;
234
235 Ok(oid_to_event_hash(commit_oid))
236 }
237
238 pub fn get_events(&self) -> Result<Vec<Event>, KelError> {
240 let ref_name = &self.ref_path;
241 let reference = self.repo.find_reference(ref_name).map_err(|e| {
242 if e.code() == ErrorCode::NotFound {
243 KelError::NotFound(self.prefix.as_str().to_string())
244 } else {
245 KelError::Git(e)
246 }
247 })?;
248
249 let mut events = Vec::new();
250 let mut commit = reference.peel_to_commit()?;
251
252 loop {
254 let event = self.read_event_from_commit(&commit)?;
255 events.push(event);
256
257 if commit.parent_count() == 0 {
258 break; }
260 commit = commit.parent(0)?;
261 }
262
263 events.reverse(); Ok(events)
265 }
266
267 pub fn get_state(&self, now: DateTime<Utc>) -> Result<KeyState, KelError> {
283 let did = format!("did:keri:{}", self.prefix.as_str());
284
285 match incremental::try_incremental_validation(self, &did, now) {
287 Ok(IncrementalResult::CacheHit(state)) => {
288 return Ok(state);
289 }
290 Ok(IncrementalResult::IncrementalSuccess {
291 state,
292 events_validated: _,
293 }) => {
294 return Ok(state);
295 }
296 Ok(IncrementalResult::NeedsFullReplay(reason)) => {
297 log::debug!("KEL full replay for {}: {:?}", did, reason);
298 }
299 Err(incremental::IncrementalError::NonLinearHistory {
300 commit,
301 parent_count,
302 }) => {
303 return Err(KelError::ChainIntegrity(format!(
305 "KEL has non-linear history: commit {} has {} parents",
306 commit, parent_count
307 )));
308 }
309 Err(e) => {
310 log::warn!("Incremental validation failed for {}: {}", did, e);
312 }
313 }
314
315 self.get_state_full_replay(now)
317 }
318
319 pub fn get_state_full_replay(&self, now: DateTime<Utc>) -> Result<KeyState, KelError> {
326 let tip_hash = self.tip_commit_hash()?;
327 let latest = self.read_event_from_commit_hash(tip_hash)?;
328 let tip_said = latest.said();
329 let tip_oid_hex = tip_hash.to_hex();
330 let did = format!("did:keri:{}", self.prefix.as_str());
331
332 let events = self.get_events()?;
333
334 if events.is_empty() {
335 return Err(KelError::NotFound(self.prefix.as_str().to_string()));
336 }
337
338 let first = &events[0];
340 let Event::Icp(icp) = first else {
341 return Err(KelError::InvalidData(
342 "First event in KEL must be inception".into(),
343 ));
344 };
345
346 let mut state = KeyState::from_inception(
347 icp.i.clone(),
348 icp.k.clone(),
349 icp.n.clone(),
350 icp.kt.clone(),
351 icp.nt.clone(),
352 icp.d.clone(),
353 icp.b.clone(),
354 icp.bt.clone(),
355 icp.c.clone(),
356 );
357
358 for event in events.iter().skip(1) {
360 match event {
361 Event::Rot(rot) => {
362 let seq = rot.s.value();
363
364 state.apply_rotation(
365 rot.k.clone(),
366 rot.n.clone(),
367 rot.kt.clone(),
368 rot.nt.clone(),
369 seq,
370 rot.d.clone(),
371 &rot.br,
372 &rot.ba,
373 rot.bt.clone(),
374 rot.c.clone(),
375 );
376 }
377 Event::Ixn(ixn) => {
378 let seq = ixn.s.value();
379 state.apply_interaction(seq, ixn.d.clone());
380 }
381 Event::Icp(_) | Event::Dip(_) => {
382 return Err(KelError::InvalidData(
383 "Multiple inception events in KEL".into(),
384 ));
385 }
386 Event::Drt(_) => {
387 return Err(KelError::InvalidData(
388 "Delegated rotation not yet supported in KEL replay".into(),
389 ));
390 }
391 }
392 }
393
394 let _ = cache::write_kel_cache(
396 self.workdir(),
397 &did,
398 &state,
399 tip_said.as_str(),
400 &tip_oid_hex,
401 now,
402 );
403
404 Ok(state)
405 }
406
407 pub fn get_latest_event(&self) -> Result<Event, KelError> {
409 let ref_name = &self.ref_path;
410 let reference = self.repo.find_reference(ref_name).map_err(|e| {
411 if e.code() == ErrorCode::NotFound {
412 KelError::NotFound(self.prefix.as_str().to_string())
413 } else {
414 KelError::Git(e)
415 }
416 })?;
417
418 let commit = reference.peel_to_commit()?;
419 self.read_event_from_commit(&commit)
420 }
421
422 fn build_current_state(&self) -> Result<KeyState, KelError> {
427 let events = self.get_events()?;
428 if events.is_empty() {
429 return Err(KelError::NotFound(self.prefix.as_str().to_string()));
430 }
431
432 let Event::Icp(icp) = &events[0] else {
433 return Err(KelError::InvalidData(
434 "First event in KEL must be inception".into(),
435 ));
436 };
437
438 let mut state = KeyState::from_inception(
439 icp.i.clone(),
440 icp.k.clone(),
441 icp.n.clone(),
442 icp.kt.clone(),
443 icp.nt.clone(),
444 icp.d.clone(),
445 icp.b.clone(),
446 icp.bt.clone(),
447 icp.c.clone(),
448 );
449
450 for event in events.iter().skip(1) {
451 match event {
452 Event::Rot(rot) => {
453 let seq = rot.s.value();
454 state.apply_rotation(
455 rot.k.clone(),
456 rot.n.clone(),
457 rot.kt.clone(),
458 rot.nt.clone(),
459 seq,
460 rot.d.clone(),
461 &rot.br,
462 &rot.ba,
463 rot.bt.clone(),
464 rot.c.clone(),
465 );
466 }
467 Event::Ixn(ixn) => {
468 state.apply_interaction(ixn.s.value(), ixn.d.clone());
469 }
470 Event::Icp(_) | Event::Dip(_) => {
471 return Err(KelError::InvalidData(
472 "Multiple inception events in KEL".into(),
473 ));
474 }
475 Event::Drt(_) => {
476 return Err(KelError::InvalidData(
477 "Delegated rotation not yet supported in KEL replay".into(),
478 ));
479 }
480 }
481 }
482
483 Ok(state)
484 }
485
486 fn read_event_from_commit(&self, commit: &Commit) -> Result<Event, KelError> {
488 let tree = commit.tree()?;
489 let entry = tree
490 .get_name(EVENT_BLOB_NAME)
491 .ok_or_else(|| KelError::InvalidData("Missing event.json in commit".into()))?;
492 let blob = self.repo.find_blob(entry.id())?;
493 let event: Event = serde_json::from_slice(blob.content())
494 .map_err(|e| KelError::Serialization(e.to_string()))?;
495 Ok(event)
496 }
497
498 fn signature(
500 &self,
501 now: chrono::DateTime<chrono::Utc>,
502 ) -> Result<Signature<'static>, KelError> {
503 self.repo
504 .signature()
505 .or_else(|_| {
506 Signature::new("auths", "auths@local", &git2::Time::new(now.timestamp(), 0))
507 })
508 .map_err(KelError::Git)
509 }
510
511 pub fn tip_commit_hash(&self) -> Result<EventHash, KelError> {
515 let ref_name = &self.ref_path;
516 let reference = self.repo.find_reference(ref_name).map_err(|e| {
517 if e.code() == ErrorCode::NotFound {
518 KelError::NotFound(self.prefix.as_str().to_string())
519 } else {
520 KelError::Git(e)
521 }
522 })?;
523 let commit = reference.peel_to_commit()?;
524 Ok(oid_to_event_hash(commit.id()))
525 }
526
527 pub fn read_event_from_commit_hash(&self, hash: EventHash) -> Result<Event, KelError> {
529 let commit = self.repo.find_commit(event_hash_to_oid(hash))?;
530 self.read_event_from_commit(&commit)
531 }
532
533 pub fn parent_hash(&self, hash: EventHash) -> Result<Option<EventHash>, KelError> {
537 let commit = self.repo.find_commit(event_hash_to_oid(hash))?;
538 if commit.parent_count() == 0 {
539 Ok(None)
540 } else {
541 Ok(Some(oid_to_event_hash(commit.parent_id(0)?)))
542 }
543 }
544
545 pub fn parent_count(&self, hash: EventHash) -> Result<usize, KelError> {
550 let commit = self.repo.find_commit(event_hash_to_oid(hash))?;
551 Ok(commit.parent_count())
552 }
553
554 pub fn commit_exists(&self, hash: EventHash) -> bool {
556 self.repo.find_commit(event_hash_to_oid(hash)).is_ok()
557 }
558
559 pub fn parse_hash(hex: &str) -> Result<EventHash, KelError> {
561 hex.parse::<EventHash>()
562 .map_err(|e| KelError::InvalidData(format!("Invalid commit hash: {}", e)))
563 }
564}
565
566#[cfg(test)]
567#[allow(clippy::disallowed_methods)]
568mod tests {
569 use super::*;
570 use crate::keri::inception::create_keri_identity_with_curve;
571 use crate::keri::rotation::rotate_keys;
572 use crate::keri::{CesrKey, KeriSequence, Prefix, RotEvent, Said, Threshold, VersionString};
573 use tempfile::TempDir;
574
575 fn setup_repo() -> (TempDir, Repository) {
576 let dir = TempDir::new().unwrap();
577 let repo = Repository::init(dir.path()).unwrap();
578
579 let mut config = repo.config().unwrap();
580 config.set_str("user.name", "Test User").unwrap();
581 config.set_str("user.email", "test@example.com").unwrap();
582
583 (dir, repo)
584 }
585
586 fn with_temp_auths_home_and_repo<F>(f: F)
587 where
588 F: FnOnce(&TempDir, &Repository),
589 {
590 let (dir, repo) = setup_repo();
591 f(&dir, &repo);
592 }
593
594 fn make_icp_event(prefix: &str) -> IcpEvent {
595 IcpEvent {
596 v: VersionString::placeholder(),
597 d: Said::new_unchecked(prefix.to_string()),
598 i: Prefix::new_unchecked(prefix.to_string()),
599 s: KeriSequence::new(0),
600 kt: Threshold::Simple(1),
601 k: vec![CesrKey::new_unchecked("DKey1".to_string())],
602 nt: Threshold::Simple(1),
603 n: vec![Said::new_unchecked("ENext1".to_string())],
604 bt: Threshold::Simple(0),
605 b: vec![],
606 c: vec![],
607 a: vec![],
608 }
609 }
610
611 #[test]
612 fn create_and_read_kel() {
613 let (_dir, repo) = setup_repo();
614 let kel = GitKel::new(&repo, "ETest123");
615
616 let icp = make_icp_event("ETest123");
617 kel.create(&icp, chrono::Utc::now()).unwrap();
618
619 assert!(kel.exists());
620
621 let events = kel.get_events().unwrap();
622 assert_eq!(events.len(), 1);
623 assert!(events[0].is_inception());
624 }
625
626 #[test]
627 fn cannot_create_duplicate_kel() {
628 let (_dir, repo) = setup_repo();
629 let kel = GitKel::new(&repo, "ETest123");
630
631 let icp = make_icp_event("ETest123");
632 kel.create(&icp, chrono::Utc::now()).unwrap();
633
634 let result = kel.create(&icp, chrono::Utc::now());
635 assert!(result.is_err());
636 }
637
638 #[test]
639 fn append_rotation_event() {
640 let (_dir, repo) = setup_repo();
641 let init = create_keri_identity_with_curve(
642 &repo,
643 None,
644 chrono::Utc::now(),
645 auths_crypto::CurveType::Ed25519,
646 )
647 .unwrap();
648 let _rot = rotate_keys(
649 &repo,
650 &init.prefix,
651 &init.next_keypair_pkcs8,
652 None,
653 chrono::Utc::now(),
654 )
655 .unwrap();
656
657 let kel = GitKel::new(&repo, init.prefix.as_str());
658 let events = kel.get_events().unwrap();
659 assert_eq!(events.len(), 2);
660 assert!(events[0].is_inception());
661 assert!(events[1].is_rotation());
662 }
663
664 #[test]
665 fn append_rejects_invalid_signature() {
666 let (_dir, repo) = setup_repo();
667 let init = create_keri_identity_with_curve(
668 &repo,
669 None,
670 chrono::Utc::now(),
671 auths_crypto::CurveType::Ed25519,
672 )
673 .unwrap();
674 let kel = GitKel::new(&repo, init.prefix.as_str());
675
676 let rot = Event::Rot(RotEvent {
678 v: VersionString::placeholder(),
679 d: Said::new_unchecked("EFakeSaid".to_string()),
680 i: init.prefix.clone(),
681 s: KeriSequence::new(1),
682 p: Said::new_unchecked(init.prefix.as_str().to_string()),
683 kt: Threshold::Simple(1),
684 k: vec![CesrKey::new_unchecked("DFakeKey".to_string())],
685 nt: Threshold::Simple(1),
686 n: vec![Said::new_unchecked("EFakeNext".to_string())],
687 bt: Threshold::Simple(0),
688 br: vec![],
689 ba: vec![],
690 c: vec![],
691 a: vec![],
692 });
693
694 let result = kel.append(&rot, chrono::Utc::now());
695 assert!(result.is_err());
696 assert!(matches!(result, Err(KelError::ValidationFailed(_))));
697 }
698
699 #[test]
700 fn get_state_after_inception() {
701 let (_dir, repo) = setup_repo();
702 let init = create_keri_identity_with_curve(
703 &repo,
704 None,
705 chrono::Utc::now(),
706 auths_crypto::CurveType::Ed25519,
707 )
708 .unwrap();
709 let kel = GitKel::new(&repo, init.prefix.as_str());
710
711 let state = kel.get_state(chrono::Utc::now()).unwrap();
712 assert_eq!(state.prefix.as_str(), init.prefix.as_str());
713 assert_eq!(state.sequence, 0);
714 assert!(state.can_rotate());
715 }
716
717 #[test]
718 fn get_state_after_rotation() {
719 let (_dir, repo) = setup_repo();
720 let init = create_keri_identity_with_curve(
721 &repo,
722 None,
723 chrono::Utc::now(),
724 auths_crypto::CurveType::Ed25519,
725 )
726 .unwrap();
727 let rot = rotate_keys(
728 &repo,
729 &init.prefix,
730 &init.next_keypair_pkcs8,
731 None,
732 chrono::Utc::now(),
733 )
734 .unwrap();
735
736 let kel = GitKel::new(&repo, init.prefix.as_str());
737 let state = kel.get_state(chrono::Utc::now()).unwrap();
738 assert_eq!(state.sequence, 1);
739 assert_eq!(rot.sequence, 1);
740 }
741
742 #[test]
743 fn get_latest_event() {
744 let (_dir, repo) = setup_repo();
745 let init = create_keri_identity_with_curve(
746 &repo,
747 None,
748 chrono::Utc::now(),
749 auths_crypto::CurveType::Ed25519,
750 )
751 .unwrap();
752 let kel = GitKel::new(&repo, init.prefix.as_str());
753
754 let latest = kel.get_latest_event().unwrap();
755 assert!(latest.is_inception());
756
757 let _rot = rotate_keys(
758 &repo,
759 &init.prefix,
760 &init.next_keypair_pkcs8,
761 None,
762 chrono::Utc::now(),
763 )
764 .unwrap();
765
766 let latest = kel.get_latest_event().unwrap();
767 assert!(latest.is_rotation());
768 }
769
770 #[test]
771 fn not_found_error_for_missing_kel() {
772 let (_dir, repo) = setup_repo();
773 let kel = GitKel::new(&repo, "ENotExist");
774
775 let result = kel.get_events();
776 assert!(matches!(result, Err(KelError::NotFound(_))));
777 }
778
779 #[test]
780 fn cannot_append_icp_event() {
781 let (_dir, repo) = setup_repo();
782 let init = create_keri_identity_with_curve(
783 &repo,
784 None,
785 chrono::Utc::now(),
786 auths_crypto::CurveType::Ed25519,
787 )
788 .unwrap();
789 let kel = GitKel::new(&repo, init.prefix.as_str());
790
791 let icp2 = Event::Icp(make_icp_event("EFake"));
792 let result = kel.append(&icp2, chrono::Utc::now());
793 assert!(matches!(result, Err(KelError::ValidationFailed(_))));
794 }
795
796 fn make_rot_event(prefix: &str, seq: u128, prev_said: &str) -> RotEvent {
799 RotEvent {
800 v: VersionString::placeholder(),
801 d: Said::new_unchecked(format!("ERot{}", seq)),
802 i: Prefix::new_unchecked(prefix.to_string()),
803 s: KeriSequence::new(seq),
804 p: Said::new_unchecked(prev_said.to_string()),
805 kt: Threshold::Simple(1),
806 k: vec![CesrKey::new_unchecked(format!("DKey{}", seq + 1))],
807 nt: Threshold::Simple(1),
808 n: vec![Said::new_unchecked(format!("ENext{}", seq + 1))],
809 bt: Threshold::Simple(0),
810 br: vec![],
811 ba: vec![],
812 c: vec![],
813 a: vec![],
814 }
815 }
816
817 #[test]
818 fn test_cold_cache_full_replay() {
819 let (_dir, repo) = setup_repo();
820 let init = create_keri_identity_with_curve(
821 &repo,
822 None,
823 chrono::Utc::now(),
824 auths_crypto::CurveType::Ed25519,
825 )
826 .unwrap();
827 let kel = GitKel::new(&repo, init.prefix.as_str());
828
829 let state = kel.get_state(chrono::Utc::now()).unwrap();
830 assert_eq!(state.prefix.as_str(), init.prefix.as_str());
831 assert_eq!(state.sequence, 0);
832
833 let did = format!("did:keri:{}", init.prefix.as_str());
834 let tip_said = kel.get_latest_event().unwrap().said().to_string();
835 let cached = cache::try_load_cached_state(_dir.path(), &did, &tip_said);
836 assert!(cached.is_some());
837 }
838
839 #[test]
840 fn test_warm_cache_hit() {
841 let (_dir, repo) = setup_repo();
842 let init = create_keri_identity_with_curve(
843 &repo,
844 None,
845 chrono::Utc::now(),
846 auths_crypto::CurveType::Ed25519,
847 )
848 .unwrap();
849 let kel = GitKel::new(&repo, init.prefix.as_str());
850
851 let state1 = kel.get_state(chrono::Utc::now()).unwrap();
852 let state2 = kel.get_state(chrono::Utc::now()).unwrap();
853
854 assert_eq!(state1, state2);
855 assert_eq!(state2.sequence, 0);
856 }
857
858 #[test]
859 fn test_incremental_validation_after_rotation() {
860 let (_dir, repo) = setup_repo();
861 let init = create_keri_identity_with_curve(
862 &repo,
863 None,
864 chrono::Utc::now(),
865 auths_crypto::CurveType::Ed25519,
866 )
867 .unwrap();
868 let kel = GitKel::new(&repo, init.prefix.as_str());
869
870 let _ = kel.get_state(chrono::Utc::now()).unwrap();
872
873 let rot1 = rotate_keys(
875 &repo,
876 &init.prefix,
877 &init.next_keypair_pkcs8,
878 None,
879 chrono::Utc::now(),
880 )
881 .unwrap();
882
883 let state = kel.get_state(chrono::Utc::now()).unwrap();
884 assert_eq!(state.sequence, 1);
885
886 let _rot2 = rotate_keys(
888 &repo,
889 &init.prefix,
890 &rot1.new_next_keypair_pkcs8,
891 None,
892 chrono::Utc::now(),
893 )
894 .unwrap();
895
896 let state = kel.get_state(chrono::Utc::now()).unwrap();
897 assert_eq!(state.sequence, 2);
898 }
899
900 #[test]
901 fn test_cache_divergence_fallback() {
902 let (_dir, repo) = setup_repo();
903 let init = create_keri_identity_with_curve(
904 &repo,
905 None,
906 chrono::Utc::now(),
907 auths_crypto::CurveType::Ed25519,
908 )
909 .unwrap();
910 let kel = GitKel::new(&repo, init.prefix.as_str());
911
912 let _ = kel.get_state(chrono::Utc::now()).unwrap();
913
914 let did = format!("did:keri:{}", init.prefix.as_str());
915 let cached_full = cache::try_load_cached_state_full(_dir.path(), &did).unwrap();
916 let _ = cache::write_kel_cache(
917 _dir.path(),
918 &did,
919 &cached_full.state,
920 cached_full.validated_against_tip_said.as_str(),
921 "0000000000000000000000000000000000000000",
922 chrono::Utc::now(),
923 );
924
925 let state = kel.get_state(chrono::Utc::now()).unwrap();
926 assert_eq!(state.prefix.as_str(), init.prefix.as_str());
927 }
928
929 #[test]
930 fn test_get_state_matches_full_replay() {
931 let (_dir, repo) = setup_repo();
932 let init = create_keri_identity_with_curve(
933 &repo,
934 None,
935 chrono::Utc::now(),
936 auths_crypto::CurveType::Ed25519,
937 )
938 .unwrap();
939 let rot1 = rotate_keys(
940 &repo,
941 &init.prefix,
942 &init.next_keypair_pkcs8,
943 None,
944 chrono::Utc::now(),
945 )
946 .unwrap();
947 let _rot2 = rotate_keys(
948 &repo,
949 &init.prefix,
950 &rot1.new_next_keypair_pkcs8,
951 None,
952 chrono::Utc::now(),
953 )
954 .unwrap();
955
956 let kel = GitKel::new(&repo, init.prefix.as_str());
957
958 let state_incremental = kel.get_state(chrono::Utc::now()).unwrap();
959 let did = format!("did:keri:{}", init.prefix.as_str());
960 let _ = cache::invalidate_cache(_dir.path(), &did);
961 let state_full = kel.get_state_full_replay(chrono::Utc::now()).unwrap();
962
963 assert_eq!(state_incremental.prefix, state_full.prefix);
964 assert_eq!(state_incremental.sequence, state_full.sequence);
965 assert_eq!(state_incremental.current_keys, state_full.current_keys);
966 assert_eq!(
967 state_incremental.last_event_said,
968 state_full.last_event_said
969 );
970 }
971
972 #[test]
973 fn test_cache_said_mismatch_forces_replay() {
974 let (_dir, repo) = setup_repo();
975 let init = create_keri_identity_with_curve(
976 &repo,
977 None,
978 chrono::Utc::now(),
979 auths_crypto::CurveType::Ed25519,
980 )
981 .unwrap();
982 let kel = GitKel::new(&repo, init.prefix.as_str());
983
984 let _ = kel.get_state(chrono::Utc::now()).unwrap();
985
986 let did = format!("did:keri:{}", init.prefix.as_str());
987 let cached_full = cache::try_load_cached_state_full(_dir.path(), &did).unwrap();
988
989 let _ = cache::write_kel_cache(
990 _dir.path(),
991 &did,
992 &cached_full.state,
993 "EFakeSaidThatDoesNotMatchCommit",
994 cached_full.last_commit_oid.as_str(),
995 chrono::Utc::now(),
996 );
997
998 let state = kel.get_state(chrono::Utc::now()).unwrap();
999 assert_eq!(state.prefix.as_str(), init.prefix.as_str());
1000 assert_eq!(state.sequence, 0);
1001
1002 let new_cached = cache::try_load_cached_state_full(_dir.path(), &did).unwrap();
1003 let tip_said = kel.get_latest_event().unwrap().said().to_string();
1004 assert_eq!(
1005 new_cached.validated_against_tip_said.as_str(),
1006 tip_said.as_str()
1007 );
1008 }
1009
1010 #[test]
1011 fn test_commit_hash_helpers() {
1012 let (_dir, repo) = setup_repo();
1013 let init = create_keri_identity_with_curve(
1014 &repo,
1015 None,
1016 chrono::Utc::now(),
1017 auths_crypto::CurveType::Ed25519,
1018 )
1019 .unwrap();
1020 let kel = GitKel::new(&repo, init.prefix.as_str());
1021
1022 let tip_hash = kel.tip_commit_hash().unwrap();
1023 assert!(kel.commit_exists(tip_hash));
1024
1025 let event = kel.read_event_from_commit_hash(tip_hash).unwrap();
1026 assert!(event.is_inception());
1027
1028 assert!(kel.parent_hash(tip_hash).unwrap().is_none());
1029
1030 let _rot = rotate_keys(
1032 &repo,
1033 &init.prefix,
1034 &init.next_keypair_pkcs8,
1035 None,
1036 chrono::Utc::now(),
1037 )
1038 .unwrap();
1039
1040 let new_tip = kel.tip_commit_hash().unwrap();
1041 assert_ne!(tip_hash, new_tip);
1042
1043 let parent = kel.parent_hash(new_tip).unwrap();
1044 assert!(parent.is_some());
1045 assert_eq!(parent.unwrap(), tip_hash);
1046 }
1047
1048 #[test]
1049 fn test_kel_merge_commit_rejected() {
1050 with_temp_auths_home_and_repo(|_repo_dir, repo| {
1051 let prefix = "EMergeReject";
1052 let kel = GitKel::new(repo, prefix);
1053 let icp = make_icp_event(prefix);
1054 kel.create(&icp, chrono::Utc::now()).unwrap();
1055
1056 let _ = kel.get_state(chrono::Utc::now()).unwrap();
1057
1058 let inception_hash = kel.tip_commit_hash().unwrap();
1059 let inception_oid = crate::witness::event_hash_to_oid(inception_hash);
1060
1061 let rot1 = Event::Rot(make_rot_event(prefix, 1, prefix));
1062 let rot1_json = serde_json::to_vec_pretty(&rot1).unwrap();
1063 let blob1_oid = repo.blob(&rot1_json).unwrap();
1064 let mut tb1 = repo.treebuilder(None).unwrap();
1065 tb1.insert("event.json", blob1_oid, 0o100644).unwrap();
1066 let tree1_oid = tb1.write().unwrap();
1067 let tree1 = repo.find_tree(tree1_oid).unwrap();
1068 let inception_commit = repo.find_commit(inception_oid).unwrap();
1069 let sig = repo.signature().unwrap();
1070 let branch1_oid = repo
1071 .commit(None, &sig, &sig, "Branch 1", &tree1, &[&inception_commit])
1072 .unwrap();
1073
1074 let rot2 = Event::Rot(make_rot_event(prefix, 1, prefix));
1076 let rot2_json = serde_json::to_vec_pretty(&rot2).unwrap();
1077 let blob2_oid = repo.blob(&rot2_json).unwrap();
1078 let mut tb2 = repo.treebuilder(None).unwrap();
1079 tb2.insert("event.json", blob2_oid, 0o100644).unwrap();
1080 let tree2_oid = tb2.write().unwrap();
1081 let tree2 = repo.find_tree(tree2_oid).unwrap();
1082 let branch2_oid = repo
1083 .commit(None, &sig, &sig, "Branch 2", &tree2, &[&inception_commit])
1084 .unwrap();
1085
1086 let branch1_commit = repo.find_commit(branch1_oid).unwrap();
1088 let branch2_commit = repo.find_commit(branch2_oid).unwrap();
1089 let merge_oid = repo
1090 .commit(
1091 None,
1092 &sig,
1093 &sig,
1094 "Merge (invalid)",
1095 &tree1,
1096 &[&branch1_commit, &branch2_commit],
1097 )
1098 .unwrap();
1099
1100 let ref_name = format!("refs/did/keri/{}/kel", prefix);
1102 repo.reference(&ref_name, merge_oid, true, "Force merge commit")
1103 .unwrap();
1104
1105 let result = kel.get_state(chrono::Utc::now());
1107 assert!(result.is_err());
1108 let err = result.unwrap_err();
1109 assert!(
1110 matches!(err, KelError::ChainIntegrity(_)),
1111 "Expected ChainIntegrity error, got: {:?}",
1112 err
1113 );
1114
1115 let msg = err.to_string();
1117 assert!(
1118 msg.contains("non-linear"),
1119 "Error message should mention non-linear: {}",
1120 msg
1121 );
1122 });
1123 }
1124}