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> {
86 repo: &'a Repository,
87 prefix: Prefix,
88 ref_path: String,
89}
90
91impl<'a> GitKel<'a> {
92 pub fn new(repo: &'a Repository, prefix: impl Into<String>) -> Self {
94 let prefix = Prefix::new_unchecked(prefix.into());
95 let ref_path = kel_ref(&prefix);
96 Self {
97 repo,
98 prefix,
99 ref_path,
100 }
101 }
102
103 pub fn with_ref(repo: &'a Repository, prefix: impl Into<String>, ref_path: String) -> Self {
117 Self {
118 repo,
119 prefix: Prefix::new_unchecked(prefix.into()),
120 ref_path,
121 }
122 }
123
124 pub fn prefix(&self) -> &Prefix {
126 &self.prefix
127 }
128
129 pub(crate) fn workdir(&self) -> &std::path::Path {
136 self.repo.workdir().unwrap_or_else(|| self.repo.path())
137 }
138
139 pub fn exists(&self) -> bool {
141 self.repo.find_reference(&self.ref_path).is_ok()
142 }
143
144 pub fn create(
158 &self,
159 event: &IcpEvent,
160 now: chrono::DateTime<chrono::Utc>,
161 ) -> Result<EventHash, KelError> {
162 if self.exists() {
163 return Err(KelError::InvalidOperation(format!(
164 "KEL already exists for prefix: {}",
165 self.prefix.as_str()
166 )));
167 }
168
169 let wrapped = Event::Icp(event.clone());
170 let json = serde_json::to_vec_pretty(&wrapped)
171 .map_err(|e| KelError::Serialization(e.to_string()))?;
172
173 let blob_oid = self.repo.blob(&json)?;
174 let mut tree_builder = self.repo.treebuilder(None)?;
175 tree_builder.insert(EVENT_BLOB_NAME, blob_oid, 0o100644)?;
176 let tree_oid = tree_builder.write()?;
177 let tree = self.repo.find_tree(tree_oid)?;
178
179 let sig = self.signature(now)?;
180 let commit_oid = self.repo.commit(
181 Some(&self.ref_path),
182 &sig,
183 &sig,
184 &format!("KERI inception: {}", event.i.as_str()),
185 &tree,
186 &[],
187 )?;
188
189 Ok(oid_to_event_hash(commit_oid))
190 }
191
192 pub fn append(
206 &self,
207 event: &Event,
208 now: chrono::DateTime<chrono::Utc>,
209 ) -> Result<EventHash, KelError> {
210 let ref_name = &self.ref_path;
211
212 let reference = self.repo.find_reference(ref_name).map_err(|e| {
213 if e.code() == ErrorCode::NotFound {
214 KelError::NotFound(self.prefix.as_str().to_string())
215 } else {
216 KelError::Git(e)
217 }
218 })?;
219 let parent_commit = reference.peel_to_commit()?;
220
221 let state = self.build_current_state()?;
223 validate_for_append(event, &state)?;
224
225 let json =
226 serde_json::to_vec_pretty(event).map_err(|e| KelError::Serialization(e.to_string()))?;
227
228 let blob_oid = self.repo.blob(&json)?;
229 let mut tree_builder = self.repo.treebuilder(None)?;
230 tree_builder.insert(EVENT_BLOB_NAME, blob_oid, 0o100644)?;
231 let tree_oid = tree_builder.write()?;
232 let tree = self.repo.find_tree(tree_oid)?;
233
234 let msg = match event {
235 Event::Rot(e) => format!("KERI rotation: s={}", e.s),
236 Event::Ixn(e) => format!("KERI interaction: s={}", e.s),
237 Event::Icp(_) => unreachable!(),
238 };
239
240 let sig = self.signature(now)?;
241 let commit_oid =
242 self.repo
243 .commit(Some(ref_name), &sig, &sig, &msg, &tree, &[&parent_commit])?;
244
245 Ok(oid_to_event_hash(commit_oid))
246 }
247
248 pub fn get_events(&self) -> Result<Vec<Event>, KelError> {
250 let ref_name = &self.ref_path;
251 let reference = self.repo.find_reference(ref_name).map_err(|e| {
252 if e.code() == ErrorCode::NotFound {
253 KelError::NotFound(self.prefix.as_str().to_string())
254 } else {
255 KelError::Git(e)
256 }
257 })?;
258
259 let mut events = Vec::new();
260 let mut commit = reference.peel_to_commit()?;
261
262 loop {
264 let event = self.read_event_from_commit(&commit)?;
265 events.push(event);
266
267 if commit.parent_count() == 0 {
268 break; }
270 commit = commit.parent(0)?;
271 }
272
273 events.reverse(); Ok(events)
275 }
276
277 pub fn get_state(&self, now: DateTime<Utc>) -> Result<KeyState, KelError> {
293 let did = format!("did:keri:{}", self.prefix.as_str());
294
295 match incremental::try_incremental_validation(self, &did, now) {
297 Ok(IncrementalResult::CacheHit(state)) => {
298 return Ok(state);
299 }
300 Ok(IncrementalResult::IncrementalSuccess {
301 state,
302 events_validated: _,
303 }) => {
304 return Ok(state);
305 }
306 Ok(IncrementalResult::NeedsFullReplay(reason)) => {
307 log::debug!("KEL full replay for {}: {:?}", did, reason);
308 }
309 Err(incremental::IncrementalError::NonLinearHistory {
310 commit,
311 parent_count,
312 }) => {
313 return Err(KelError::ChainIntegrity(format!(
315 "KEL has non-linear history: commit {} has {} parents",
316 commit, parent_count
317 )));
318 }
319 Err(e) => {
320 log::warn!("Incremental validation failed for {}: {}", did, e);
322 }
323 }
324
325 self.get_state_full_replay(now)
327 }
328
329 pub fn get_state_full_replay(&self, now: DateTime<Utc>) -> Result<KeyState, KelError> {
336 let tip_hash = self.tip_commit_hash()?;
337 let latest = self.read_event_from_commit_hash(tip_hash)?;
338 let tip_said = latest.said();
339 let tip_oid_hex = tip_hash.to_hex();
340 let did = format!("did:keri:{}", self.prefix.as_str());
341
342 let events = self.get_events()?;
343
344 if events.is_empty() {
345 return Err(KelError::NotFound(self.prefix.as_str().to_string()));
346 }
347
348 let first = &events[0];
350 let Event::Icp(icp) = first else {
351 return Err(KelError::InvalidData(
352 "First event in KEL must be inception".into(),
353 ));
354 };
355
356 let threshold = icp.kt.parse::<u64>().map_err(|_| {
357 KelError::InvalidData(format!("Malformed sequence number: {:?}", icp.kt))
358 })?;
359 let next_threshold = icp.nt.parse::<u64>().map_err(|_| {
360 KelError::InvalidData(format!("Malformed sequence number: {:?}", icp.nt))
361 })?;
362
363 let mut state = KeyState::from_inception(
364 icp.i.clone(),
365 icp.k.clone(),
366 icp.n.clone(),
367 threshold,
368 next_threshold,
369 icp.d.clone(),
370 );
371
372 for event in events.iter().skip(1) {
374 match event {
375 Event::Rot(rot) => {
376 let seq = rot.s.value();
377 let threshold = rot.kt.parse::<u64>().map_err(|_| {
378 KelError::InvalidData(format!("Malformed sequence number: {:?}", rot.kt))
379 })?;
380 let next_threshold = rot.nt.parse::<u64>().map_err(|_| {
381 KelError::InvalidData(format!("Malformed sequence number: {:?}", rot.nt))
382 })?;
383
384 state.apply_rotation(
385 rot.k.clone(),
386 rot.n.clone(),
387 threshold,
388 next_threshold,
389 seq,
390 rot.d.clone(),
391 );
392 }
393 Event::Ixn(ixn) => {
394 let seq = ixn.s.value();
395 state.apply_interaction(seq, ixn.d.clone());
396 }
397 Event::Icp(_) => {
398 return Err(KelError::InvalidData(
399 "Multiple inception events in KEL".into(),
400 ));
401 }
402 }
403 }
404
405 let _ = cache::write_kel_cache(
407 self.workdir(),
408 &did,
409 &state,
410 tip_said.as_str(),
411 &tip_oid_hex,
412 now,
413 );
414
415 Ok(state)
416 }
417
418 pub fn get_latest_event(&self) -> Result<Event, KelError> {
420 let ref_name = &self.ref_path;
421 let reference = self.repo.find_reference(ref_name).map_err(|e| {
422 if e.code() == ErrorCode::NotFound {
423 KelError::NotFound(self.prefix.as_str().to_string())
424 } else {
425 KelError::Git(e)
426 }
427 })?;
428
429 let commit = reference.peel_to_commit()?;
430 self.read_event_from_commit(&commit)
431 }
432
433 fn build_current_state(&self) -> Result<KeyState, KelError> {
438 let events = self.get_events()?;
439 if events.is_empty() {
440 return Err(KelError::NotFound(self.prefix.as_str().to_string()));
441 }
442
443 let Event::Icp(icp) = &events[0] else {
444 return Err(KelError::InvalidData(
445 "First event in KEL must be inception".into(),
446 ));
447 };
448
449 let threshold = icp
450 .kt
451 .parse::<u64>()
452 .map_err(|_| KelError::InvalidData(format!("Malformed threshold: {:?}", icp.kt)))?;
453 let next_threshold = icp
454 .nt
455 .parse::<u64>()
456 .map_err(|_| KelError::InvalidData(format!("Malformed threshold: {:?}", icp.nt)))?;
457
458 let mut state = KeyState::from_inception(
459 icp.i.clone(),
460 icp.k.clone(),
461 icp.n.clone(),
462 threshold,
463 next_threshold,
464 icp.d.clone(),
465 );
466
467 for event in events.iter().skip(1) {
468 match event {
469 Event::Rot(rot) => {
470 let seq = rot.s.value();
471 let t = rot.kt.parse::<u64>().map_err(|_| {
472 KelError::InvalidData(format!("Malformed threshold: {:?}", rot.kt))
473 })?;
474 let nt = rot.nt.parse::<u64>().map_err(|_| {
475 KelError::InvalidData(format!("Malformed threshold: {:?}", rot.nt))
476 })?;
477 state.apply_rotation(rot.k.clone(), rot.n.clone(), t, nt, seq, rot.d.clone());
478 }
479 Event::Ixn(ixn) => {
480 state.apply_interaction(ixn.s.value(), ixn.d.clone());
481 }
482 Event::Icp(_) => {
483 return Err(KelError::InvalidData(
484 "Multiple inception events in KEL".into(),
485 ));
486 }
487 }
488 }
489
490 Ok(state)
491 }
492
493 fn read_event_from_commit(&self, commit: &Commit) -> Result<Event, KelError> {
495 let tree = commit.tree()?;
496 let entry = tree
497 .get_name(EVENT_BLOB_NAME)
498 .ok_or_else(|| KelError::InvalidData("Missing event.json in commit".into()))?;
499 let blob = self.repo.find_blob(entry.id())?;
500 let event: Event = serde_json::from_slice(blob.content())
501 .map_err(|e| KelError::Serialization(e.to_string()))?;
502 Ok(event)
503 }
504
505 fn signature(
507 &self,
508 now: chrono::DateTime<chrono::Utc>,
509 ) -> Result<Signature<'static>, KelError> {
510 self.repo
511 .signature()
512 .or_else(|_| {
513 Signature::new("auths", "auths@local", &git2::Time::new(now.timestamp(), 0))
514 })
515 .map_err(KelError::Git)
516 }
517
518 pub fn tip_commit_hash(&self) -> Result<EventHash, KelError> {
522 let ref_name = &self.ref_path;
523 let reference = self.repo.find_reference(ref_name).map_err(|e| {
524 if e.code() == ErrorCode::NotFound {
525 KelError::NotFound(self.prefix.as_str().to_string())
526 } else {
527 KelError::Git(e)
528 }
529 })?;
530 let commit = reference.peel_to_commit()?;
531 Ok(oid_to_event_hash(commit.id()))
532 }
533
534 pub fn read_event_from_commit_hash(&self, hash: EventHash) -> Result<Event, KelError> {
536 let commit = self.repo.find_commit(event_hash_to_oid(hash))?;
537 self.read_event_from_commit(&commit)
538 }
539
540 pub fn parent_hash(&self, hash: EventHash) -> Result<Option<EventHash>, KelError> {
544 let commit = self.repo.find_commit(event_hash_to_oid(hash))?;
545 if commit.parent_count() == 0 {
546 Ok(None)
547 } else {
548 Ok(Some(oid_to_event_hash(commit.parent_id(0)?)))
549 }
550 }
551
552 pub fn parent_count(&self, hash: EventHash) -> Result<usize, KelError> {
557 let commit = self.repo.find_commit(event_hash_to_oid(hash))?;
558 Ok(commit.parent_count())
559 }
560
561 pub fn commit_exists(&self, hash: EventHash) -> bool {
563 self.repo.find_commit(event_hash_to_oid(hash)).is_ok()
564 }
565
566 pub fn parse_hash(hex: &str) -> Result<EventHash, KelError> {
568 hex.parse::<EventHash>()
569 .map_err(|e| KelError::InvalidData(format!("Invalid commit hash: {}", e)))
570 }
571}
572
573#[cfg(test)]
574#[allow(clippy::disallowed_methods)]
575mod tests {
576 use super::*;
577 use crate::keri::inception::create_keri_identity;
578 use crate::keri::rotation::rotate_keys;
579 use crate::keri::{KERI_VERSION, KeriSequence, Prefix, RotEvent, Said};
580 use tempfile::TempDir;
581
582 fn setup_repo() -> (TempDir, Repository) {
583 let dir = TempDir::new().unwrap();
584 let repo = Repository::init(dir.path()).unwrap();
585
586 let mut config = repo.config().unwrap();
587 config.set_str("user.name", "Test User").unwrap();
588 config.set_str("user.email", "test@example.com").unwrap();
589
590 (dir, repo)
591 }
592
593 fn with_temp_auths_home_and_repo<F>(f: F)
594 where
595 F: FnOnce(&TempDir, &Repository),
596 {
597 let (dir, repo) = setup_repo();
598 f(&dir, &repo);
599 }
600
601 fn make_icp_event(prefix: &str) -> IcpEvent {
602 IcpEvent {
603 v: KERI_VERSION.to_string(),
604 d: Said::new_unchecked(prefix.to_string()),
605 i: Prefix::new_unchecked(prefix.to_string()),
606 s: KeriSequence::new(0),
607 kt: "1".to_string(),
608 k: vec!["DKey1".to_string()],
609 nt: "1".to_string(),
610 n: vec!["ENext1".to_string()],
611 bt: "0".to_string(),
612 b: vec![],
613 a: vec![],
614 x: String::new(),
615 }
616 }
617
618 #[test]
619 fn create_and_read_kel() {
620 let (_dir, repo) = setup_repo();
621 let kel = GitKel::new(&repo, "ETest123");
622
623 let icp = make_icp_event("ETest123");
624 kel.create(&icp, chrono::Utc::now()).unwrap();
625
626 assert!(kel.exists());
627
628 let events = kel.get_events().unwrap();
629 assert_eq!(events.len(), 1);
630 assert!(events[0].is_inception());
631 }
632
633 #[test]
634 fn cannot_create_duplicate_kel() {
635 let (_dir, repo) = setup_repo();
636 let kel = GitKel::new(&repo, "ETest123");
637
638 let icp = make_icp_event("ETest123");
639 kel.create(&icp, chrono::Utc::now()).unwrap();
640
641 let result = kel.create(&icp, chrono::Utc::now());
642 assert!(result.is_err());
643 }
644
645 #[test]
646 fn append_rotation_event() {
647 let (_dir, repo) = setup_repo();
648 let init = create_keri_identity(&repo, None, chrono::Utc::now()).unwrap();
649 let _rot = rotate_keys(
650 &repo,
651 &init.prefix,
652 &init.next_keypair_pkcs8,
653 None,
654 chrono::Utc::now(),
655 )
656 .unwrap();
657
658 let kel = GitKel::new(&repo, init.prefix.as_str());
659 let events = kel.get_events().unwrap();
660 assert_eq!(events.len(), 2);
661 assert!(events[0].is_inception());
662 assert!(events[1].is_rotation());
663 }
664
665 #[test]
666 fn append_rejects_invalid_signature() {
667 let (_dir, repo) = setup_repo();
668 let init = create_keri_identity(&repo, None, chrono::Utc::now()).unwrap();
669 let kel = GitKel::new(&repo, init.prefix.as_str());
670
671 let rot = Event::Rot(RotEvent {
673 v: KERI_VERSION.to_string(),
674 d: Said::new_unchecked("EFakeSaid".to_string()),
675 i: init.prefix.clone(),
676 s: KeriSequence::new(1),
677 p: Said::new_unchecked(init.prefix.as_str().to_string()),
678 kt: "1".to_string(),
679 k: vec!["DFakeKey".to_string()],
680 nt: "1".to_string(),
681 n: vec!["EFakeNext".to_string()],
682 bt: "0".to_string(),
683 b: vec![],
684 a: vec![],
685 x: String::new(),
686 });
687
688 let result = kel.append(&rot, chrono::Utc::now());
689 assert!(result.is_err());
690 assert!(matches!(result, Err(KelError::ValidationFailed(_))));
691 }
692
693 #[test]
694 fn get_state_after_inception() {
695 let (_dir, repo) = setup_repo();
696 let init = create_keri_identity(&repo, None, chrono::Utc::now()).unwrap();
697 let kel = GitKel::new(&repo, init.prefix.as_str());
698
699 let state = kel.get_state(chrono::Utc::now()).unwrap();
700 assert_eq!(state.prefix.as_str(), init.prefix.as_str());
701 assert_eq!(state.sequence, 0);
702 assert!(state.can_rotate());
703 }
704
705 #[test]
706 fn get_state_after_rotation() {
707 let (_dir, repo) = setup_repo();
708 let init = create_keri_identity(&repo, None, chrono::Utc::now()).unwrap();
709 let rot = rotate_keys(
710 &repo,
711 &init.prefix,
712 &init.next_keypair_pkcs8,
713 None,
714 chrono::Utc::now(),
715 )
716 .unwrap();
717
718 let kel = GitKel::new(&repo, init.prefix.as_str());
719 let state = kel.get_state(chrono::Utc::now()).unwrap();
720 assert_eq!(state.sequence, 1);
721 assert_eq!(rot.sequence, 1);
722 }
723
724 #[test]
725 fn get_latest_event() {
726 let (_dir, repo) = setup_repo();
727 let init = create_keri_identity(&repo, None, chrono::Utc::now()).unwrap();
728 let kel = GitKel::new(&repo, init.prefix.as_str());
729
730 let latest = kel.get_latest_event().unwrap();
731 assert!(latest.is_inception());
732
733 let _rot = rotate_keys(
734 &repo,
735 &init.prefix,
736 &init.next_keypair_pkcs8,
737 None,
738 chrono::Utc::now(),
739 )
740 .unwrap();
741
742 let latest = kel.get_latest_event().unwrap();
743 assert!(latest.is_rotation());
744 }
745
746 #[test]
747 fn not_found_error_for_missing_kel() {
748 let (_dir, repo) = setup_repo();
749 let kel = GitKel::new(&repo, "ENotExist");
750
751 let result = kel.get_events();
752 assert!(matches!(result, Err(KelError::NotFound(_))));
753 }
754
755 #[test]
756 fn cannot_append_icp_event() {
757 let (_dir, repo) = setup_repo();
758 let init = create_keri_identity(&repo, None, chrono::Utc::now()).unwrap();
759 let kel = GitKel::new(&repo, init.prefix.as_str());
760
761 let icp2 = Event::Icp(make_icp_event("EFake"));
762 let result = kel.append(&icp2, chrono::Utc::now());
763 assert!(matches!(result, Err(KelError::ValidationFailed(_))));
764 }
765
766 fn make_rot_event(prefix: &str, seq: u64, prev_said: &str) -> RotEvent {
769 RotEvent {
770 v: KERI_VERSION.to_string(),
771 d: Said::new_unchecked(format!("ERot{}", seq)),
772 i: Prefix::new_unchecked(prefix.to_string()),
773 s: KeriSequence::new(seq),
774 p: Said::new_unchecked(prev_said.to_string()),
775 kt: "1".to_string(),
776 k: vec![format!("DKey{}", seq + 1)],
777 nt: "1".to_string(),
778 n: vec![format!("ENext{}", seq + 1)],
779 bt: "0".to_string(),
780 b: vec![],
781 a: vec![],
782 x: String::new(),
783 }
784 }
785
786 #[test]
787 fn test_cold_cache_full_replay() {
788 let (_dir, repo) = setup_repo();
789 let init = create_keri_identity(&repo, None, chrono::Utc::now()).unwrap();
790 let kel = GitKel::new(&repo, init.prefix.as_str());
791
792 let state = kel.get_state(chrono::Utc::now()).unwrap();
793 assert_eq!(state.prefix.as_str(), init.prefix.as_str());
794 assert_eq!(state.sequence, 0);
795
796 let did = format!("did:keri:{}", init.prefix.as_str());
797 let tip_said = kel.get_latest_event().unwrap().said().to_string();
798 let cached = cache::try_load_cached_state(_dir.path(), &did, &tip_said);
799 assert!(cached.is_some());
800 }
801
802 #[test]
803 fn test_warm_cache_hit() {
804 let (_dir, repo) = setup_repo();
805 let init = create_keri_identity(&repo, None, chrono::Utc::now()).unwrap();
806 let kel = GitKel::new(&repo, init.prefix.as_str());
807
808 let state1 = kel.get_state(chrono::Utc::now()).unwrap();
809 let state2 = kel.get_state(chrono::Utc::now()).unwrap();
810
811 assert_eq!(state1, state2);
812 assert_eq!(state2.sequence, 0);
813 }
814
815 #[test]
816 fn test_incremental_validation_after_rotation() {
817 let (_dir, repo) = setup_repo();
818 let init = create_keri_identity(&repo, None, chrono::Utc::now()).unwrap();
819 let kel = GitKel::new(&repo, init.prefix.as_str());
820
821 let _ = kel.get_state(chrono::Utc::now()).unwrap();
823
824 let rot1 = rotate_keys(
826 &repo,
827 &init.prefix,
828 &init.next_keypair_pkcs8,
829 None,
830 chrono::Utc::now(),
831 )
832 .unwrap();
833
834 let state = kel.get_state(chrono::Utc::now()).unwrap();
835 assert_eq!(state.sequence, 1);
836
837 let _rot2 = rotate_keys(
839 &repo,
840 &init.prefix,
841 &rot1.new_next_keypair_pkcs8,
842 None,
843 chrono::Utc::now(),
844 )
845 .unwrap();
846
847 let state = kel.get_state(chrono::Utc::now()).unwrap();
848 assert_eq!(state.sequence, 2);
849 }
850
851 #[test]
852 fn test_cache_divergence_fallback() {
853 let (_dir, repo) = setup_repo();
854 let init = create_keri_identity(&repo, None, chrono::Utc::now()).unwrap();
855 let kel = GitKel::new(&repo, init.prefix.as_str());
856
857 let _ = kel.get_state(chrono::Utc::now()).unwrap();
858
859 let did = format!("did:keri:{}", init.prefix.as_str());
860 let cached_full = cache::try_load_cached_state_full(_dir.path(), &did).unwrap();
861 let _ = cache::write_kel_cache(
862 _dir.path(),
863 &did,
864 &cached_full.state,
865 cached_full.validated_against_tip_said.as_str(),
866 "0000000000000000000000000000000000000000",
867 chrono::Utc::now(),
868 );
869
870 let state = kel.get_state(chrono::Utc::now()).unwrap();
871 assert_eq!(state.prefix.as_str(), init.prefix.as_str());
872 }
873
874 #[test]
875 fn test_get_state_matches_full_replay() {
876 let (_dir, repo) = setup_repo();
877 let init = create_keri_identity(&repo, None, chrono::Utc::now()).unwrap();
878 let rot1 = rotate_keys(
879 &repo,
880 &init.prefix,
881 &init.next_keypair_pkcs8,
882 None,
883 chrono::Utc::now(),
884 )
885 .unwrap();
886 let _rot2 = rotate_keys(
887 &repo,
888 &init.prefix,
889 &rot1.new_next_keypair_pkcs8,
890 None,
891 chrono::Utc::now(),
892 )
893 .unwrap();
894
895 let kel = GitKel::new(&repo, init.prefix.as_str());
896
897 let state_incremental = kel.get_state(chrono::Utc::now()).unwrap();
898 let did = format!("did:keri:{}", init.prefix.as_str());
899 let _ = cache::invalidate_cache(_dir.path(), &did);
900 let state_full = kel.get_state_full_replay(chrono::Utc::now()).unwrap();
901
902 assert_eq!(state_incremental.prefix, state_full.prefix);
903 assert_eq!(state_incremental.sequence, state_full.sequence);
904 assert_eq!(state_incremental.current_keys, state_full.current_keys);
905 assert_eq!(
906 state_incremental.last_event_said,
907 state_full.last_event_said
908 );
909 }
910
911 #[test]
912 fn test_cache_said_mismatch_forces_replay() {
913 let (_dir, repo) = setup_repo();
914 let init = create_keri_identity(&repo, None, chrono::Utc::now()).unwrap();
915 let kel = GitKel::new(&repo, init.prefix.as_str());
916
917 let _ = kel.get_state(chrono::Utc::now()).unwrap();
918
919 let did = format!("did:keri:{}", init.prefix.as_str());
920 let cached_full = cache::try_load_cached_state_full(_dir.path(), &did).unwrap();
921
922 let _ = cache::write_kel_cache(
923 _dir.path(),
924 &did,
925 &cached_full.state,
926 "EFakeSaidThatDoesNotMatchCommit",
927 cached_full.last_commit_oid.as_str(),
928 chrono::Utc::now(),
929 );
930
931 let state = kel.get_state(chrono::Utc::now()).unwrap();
932 assert_eq!(state.prefix.as_str(), init.prefix.as_str());
933 assert_eq!(state.sequence, 0);
934
935 let new_cached = cache::try_load_cached_state_full(_dir.path(), &did).unwrap();
936 let tip_said = kel.get_latest_event().unwrap().said().to_string();
937 assert_eq!(
938 new_cached.validated_against_tip_said.as_str(),
939 tip_said.as_str()
940 );
941 }
942
943 #[test]
944 fn test_commit_hash_helpers() {
945 let (_dir, repo) = setup_repo();
946 let init = create_keri_identity(&repo, None, chrono::Utc::now()).unwrap();
947 let kel = GitKel::new(&repo, init.prefix.as_str());
948
949 let tip_hash = kel.tip_commit_hash().unwrap();
950 assert!(kel.commit_exists(tip_hash));
951
952 let event = kel.read_event_from_commit_hash(tip_hash).unwrap();
953 assert!(event.is_inception());
954
955 assert!(kel.parent_hash(tip_hash).unwrap().is_none());
956
957 let _rot = rotate_keys(
959 &repo,
960 &init.prefix,
961 &init.next_keypair_pkcs8,
962 None,
963 chrono::Utc::now(),
964 )
965 .unwrap();
966
967 let new_tip = kel.tip_commit_hash().unwrap();
968 assert_ne!(tip_hash, new_tip);
969
970 let parent = kel.parent_hash(new_tip).unwrap();
971 assert!(parent.is_some());
972 assert_eq!(parent.unwrap(), tip_hash);
973 }
974
975 #[test]
976 fn test_kel_merge_commit_rejected() {
977 with_temp_auths_home_and_repo(|_repo_dir, repo| {
978 let prefix = "EMergeReject";
979 let kel = GitKel::new(repo, prefix);
980 let icp = make_icp_event(prefix);
981 kel.create(&icp, chrono::Utc::now()).unwrap();
982
983 let _ = kel.get_state(chrono::Utc::now()).unwrap();
984
985 let inception_hash = kel.tip_commit_hash().unwrap();
986 let inception_oid = crate::witness::event_hash_to_oid(inception_hash);
987
988 let rot1 = Event::Rot(make_rot_event(prefix, 1, prefix));
989 let rot1_json = serde_json::to_vec_pretty(&rot1).unwrap();
990 let blob1_oid = repo.blob(&rot1_json).unwrap();
991 let mut tb1 = repo.treebuilder(None).unwrap();
992 tb1.insert("event.json", blob1_oid, 0o100644).unwrap();
993 let tree1_oid = tb1.write().unwrap();
994 let tree1 = repo.find_tree(tree1_oid).unwrap();
995 let inception_commit = repo.find_commit(inception_oid).unwrap();
996 let sig = repo.signature().unwrap();
997 let branch1_oid = repo
998 .commit(None, &sig, &sig, "Branch 1", &tree1, &[&inception_commit])
999 .unwrap();
1000
1001 let rot2 = Event::Rot(make_rot_event(prefix, 1, prefix));
1003 let rot2_json = serde_json::to_vec_pretty(&rot2).unwrap();
1004 let blob2_oid = repo.blob(&rot2_json).unwrap();
1005 let mut tb2 = repo.treebuilder(None).unwrap();
1006 tb2.insert("event.json", blob2_oid, 0o100644).unwrap();
1007 let tree2_oid = tb2.write().unwrap();
1008 let tree2 = repo.find_tree(tree2_oid).unwrap();
1009 let branch2_oid = repo
1010 .commit(None, &sig, &sig, "Branch 2", &tree2, &[&inception_commit])
1011 .unwrap();
1012
1013 let branch1_commit = repo.find_commit(branch1_oid).unwrap();
1015 let branch2_commit = repo.find_commit(branch2_oid).unwrap();
1016 let merge_oid = repo
1017 .commit(
1018 None,
1019 &sig,
1020 &sig,
1021 "Merge (invalid)",
1022 &tree1,
1023 &[&branch1_commit, &branch2_commit],
1024 )
1025 .unwrap();
1026
1027 let ref_name = format!("refs/did/keri/{}/kel", prefix);
1029 repo.reference(&ref_name, merge_oid, true, "Force merge commit")
1030 .unwrap();
1031
1032 let result = kel.get_state(chrono::Utc::now());
1034 assert!(result.is_err());
1035 let err = result.unwrap_err();
1036 assert!(
1037 matches!(err, KelError::ChainIntegrity(_)),
1038 "Expected ChainIntegrity error, got: {:?}",
1039 err
1040 );
1041
1042 let msg = err.to_string();
1044 assert!(
1045 msg.contains("non-linear"),
1046 "Error message should mention non-linear: {}",
1047 msg
1048 );
1049 });
1050 }
1051}