use chrono::{DateTime, Utc};
use git2::{Commit, ErrorCode, Repository, Signature};
use super::cache;
use super::incremental::{self, IncrementalResult};
use super::types::Prefix;
use super::validate::validate_for_append;
use super::{Event, IcpEvent, KeyState};
use crate::domain::EventHash;
use crate::witness::{event_hash_to_oid, oid_to_event_hash};
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum KelError {
#[error("Git error: {0}")]
Git(#[from] git2::Error),
#[error("Serialization error: {0}")]
Serialization(String),
#[error("KEL not found for prefix: {0}")]
NotFound(String),
#[error("Invalid operation: {0}")]
InvalidOperation(String),
#[error("Invalid data: {0}")]
InvalidData(String),
#[error("Chain integrity error: {0}")]
ChainIntegrity(String),
#[error("Validation failed: {0}")]
ValidationFailed(#[from] super::ValidationError),
}
impl auths_core::error::AuthsErrorInfo for KelError {
fn error_code(&self) -> &'static str {
match self {
Self::Git(_) => "AUTHS-E4601",
Self::Serialization(_) => "AUTHS-E4602",
Self::NotFound(_) => "AUTHS-E4603",
Self::InvalidOperation(_) => "AUTHS-E4604",
Self::InvalidData(_) => "AUTHS-E4605",
Self::ChainIntegrity(_) => "AUTHS-E4606",
Self::ValidationFailed(_) => "AUTHS-E4607",
}
}
fn suggestion(&self) -> Option<&'static str> {
match self {
Self::Git(_) => Some("Check that the Git repository is accessible and not corrupted"),
Self::Serialization(_) => None,
Self::NotFound(_) => Some("Initialize the identity first with 'auths init'"),
Self::InvalidOperation(_) => None,
Self::InvalidData(_) => Some("The KEL data may be corrupted; try re-syncing"),
Self::ChainIntegrity(_) => {
Some("The KEL has non-linear history; this indicates tampering")
}
Self::ValidationFailed(_) => None,
}
}
}
const EVENT_BLOB_NAME: &str = "event.json";
fn kel_ref(prefix: &Prefix) -> String {
format!("refs/did/keri/{}/kel", prefix.as_str())
}
pub struct GitKel<'a> {
repo: &'a Repository,
prefix: Prefix,
ref_path: String,
}
impl<'a> GitKel<'a> {
pub fn new(repo: &'a Repository, prefix: impl Into<String>) -> Self {
let prefix = Prefix::new_unchecked(prefix.into());
let ref_path = kel_ref(&prefix);
Self {
repo,
prefix,
ref_path,
}
}
pub fn with_ref(repo: &'a Repository, prefix: impl Into<String>, ref_path: String) -> Self {
Self {
repo,
prefix: Prefix::new_unchecked(prefix.into()),
ref_path,
}
}
pub fn prefix(&self) -> &Prefix {
&self.prefix
}
pub(crate) fn workdir(&self) -> &std::path::Path {
self.repo.workdir().unwrap_or_else(|| self.repo.path())
}
pub fn exists(&self) -> bool {
self.repo.find_reference(&self.ref_path).is_ok()
}
pub fn create(
&self,
event: &IcpEvent,
now: chrono::DateTime<chrono::Utc>,
) -> Result<EventHash, KelError> {
if self.exists() {
return Err(KelError::InvalidOperation(format!(
"KEL already exists for prefix: {}",
self.prefix.as_str()
)));
}
let wrapped = Event::Icp(event.clone());
let json = serde_json::to_vec_pretty(&wrapped)
.map_err(|e| KelError::Serialization(e.to_string()))?;
let blob_oid = self.repo.blob(&json)?;
let mut tree_builder = self.repo.treebuilder(None)?;
tree_builder.insert(EVENT_BLOB_NAME, blob_oid, 0o100644)?;
let tree_oid = tree_builder.write()?;
let tree = self.repo.find_tree(tree_oid)?;
let sig = self.signature(now)?;
let commit_oid = self.repo.commit(
Some(&self.ref_path),
&sig,
&sig,
&format!("KERI inception: {}", event.i.as_str()),
&tree,
&[],
)?;
Ok(oid_to_event_hash(commit_oid))
}
pub fn append(
&self,
event: &Event,
now: chrono::DateTime<chrono::Utc>,
) -> Result<EventHash, KelError> {
let ref_name = &self.ref_path;
let reference = self.repo.find_reference(ref_name).map_err(|e| {
if e.code() == ErrorCode::NotFound {
KelError::NotFound(self.prefix.as_str().to_string())
} else {
KelError::Git(e)
}
})?;
let parent_commit = reference.peel_to_commit()?;
let state = self.build_current_state()?;
validate_for_append(event, &state)?;
let json =
serde_json::to_vec_pretty(event).map_err(|e| KelError::Serialization(e.to_string()))?;
let blob_oid = self.repo.blob(&json)?;
let mut tree_builder = self.repo.treebuilder(None)?;
tree_builder.insert(EVENT_BLOB_NAME, blob_oid, 0o100644)?;
let tree_oid = tree_builder.write()?;
let tree = self.repo.find_tree(tree_oid)?;
let msg = match event {
Event::Rot(e) => format!("KERI rotation: s={}", e.s),
Event::Ixn(e) => format!("KERI interaction: s={}", e.s),
Event::Drt(e) => format!("KERI delegated rotation: s={}", e.s),
Event::Icp(_) | Event::Dip(_) => unreachable!(),
};
let sig = self.signature(now)?;
let commit_oid =
self.repo
.commit(Some(ref_name), &sig, &sig, &msg, &tree, &[&parent_commit])?;
Ok(oid_to_event_hash(commit_oid))
}
pub fn get_events(&self) -> Result<Vec<Event>, KelError> {
let ref_name = &self.ref_path;
let reference = self.repo.find_reference(ref_name).map_err(|e| {
if e.code() == ErrorCode::NotFound {
KelError::NotFound(self.prefix.as_str().to_string())
} else {
KelError::Git(e)
}
})?;
let mut events = Vec::new();
let mut commit = reference.peel_to_commit()?;
loop {
let event = self.read_event_from_commit(&commit)?;
events.push(event);
if commit.parent_count() == 0 {
break; }
commit = commit.parent(0)?;
}
events.reverse(); Ok(events)
}
pub fn get_state(&self, now: DateTime<Utc>) -> Result<KeyState, KelError> {
let did = format!("did:keri:{}", self.prefix.as_str());
match incremental::try_incremental_validation(self, &did, now) {
Ok(IncrementalResult::CacheHit(state)) => {
return Ok(state);
}
Ok(IncrementalResult::IncrementalSuccess {
state,
events_validated: _,
}) => {
return Ok(state);
}
Ok(IncrementalResult::NeedsFullReplay(reason)) => {
log::debug!("KEL full replay for {}: {:?}", did, reason);
}
Err(incremental::IncrementalError::NonLinearHistory {
commit,
parent_count,
}) => {
return Err(KelError::ChainIntegrity(format!(
"KEL has non-linear history: commit {} has {} parents",
commit, parent_count
)));
}
Err(e) => {
log::warn!("Incremental validation failed for {}: {}", did, e);
}
}
self.get_state_full_replay(now)
}
pub fn get_state_full_replay(&self, now: DateTime<Utc>) -> Result<KeyState, KelError> {
let tip_hash = self.tip_commit_hash()?;
let latest = self.read_event_from_commit_hash(tip_hash)?;
let tip_said = latest.said();
let tip_oid_hex = tip_hash.to_hex();
let did = format!("did:keri:{}", self.prefix.as_str());
let events = self.get_events()?;
if events.is_empty() {
return Err(KelError::NotFound(self.prefix.as_str().to_string()));
}
let first = &events[0];
let Event::Icp(icp) = first else {
return Err(KelError::InvalidData(
"First event in KEL must be inception".into(),
));
};
let mut state = KeyState::from_inception(
icp.i.clone(),
icp.k.clone(),
icp.n.clone(),
icp.kt.clone(),
icp.nt.clone(),
icp.d.clone(),
icp.b.clone(),
icp.bt.clone(),
icp.c.clone(),
);
for event in events.iter().skip(1) {
match event {
Event::Rot(rot) => {
let seq = rot.s.value();
state.apply_rotation(
rot.k.clone(),
rot.n.clone(),
rot.kt.clone(),
rot.nt.clone(),
seq,
rot.d.clone(),
&rot.br,
&rot.ba,
rot.bt.clone(),
rot.c.clone(),
);
}
Event::Ixn(ixn) => {
let seq = ixn.s.value();
state.apply_interaction(seq, ixn.d.clone());
}
Event::Icp(_) | Event::Dip(_) => {
return Err(KelError::InvalidData(
"Multiple inception events in KEL".into(),
));
}
Event::Drt(_) => {
return Err(KelError::InvalidData(
"Delegated rotation not yet supported in KEL replay".into(),
));
}
}
}
let _ = cache::write_kel_cache(
self.workdir(),
&did,
&state,
tip_said.as_str(),
&tip_oid_hex,
now,
);
Ok(state)
}
pub fn get_latest_event(&self) -> Result<Event, KelError> {
let ref_name = &self.ref_path;
let reference = self.repo.find_reference(ref_name).map_err(|e| {
if e.code() == ErrorCode::NotFound {
KelError::NotFound(self.prefix.as_str().to_string())
} else {
KelError::Git(e)
}
})?;
let commit = reference.peel_to_commit()?;
self.read_event_from_commit(&commit)
}
fn build_current_state(&self) -> Result<KeyState, KelError> {
let events = self.get_events()?;
if events.is_empty() {
return Err(KelError::NotFound(self.prefix.as_str().to_string()));
}
let Event::Icp(icp) = &events[0] else {
return Err(KelError::InvalidData(
"First event in KEL must be inception".into(),
));
};
let mut state = KeyState::from_inception(
icp.i.clone(),
icp.k.clone(),
icp.n.clone(),
icp.kt.clone(),
icp.nt.clone(),
icp.d.clone(),
icp.b.clone(),
icp.bt.clone(),
icp.c.clone(),
);
for event in events.iter().skip(1) {
match event {
Event::Rot(rot) => {
let seq = rot.s.value();
state.apply_rotation(
rot.k.clone(),
rot.n.clone(),
rot.kt.clone(),
rot.nt.clone(),
seq,
rot.d.clone(),
&rot.br,
&rot.ba,
rot.bt.clone(),
rot.c.clone(),
);
}
Event::Ixn(ixn) => {
state.apply_interaction(ixn.s.value(), ixn.d.clone());
}
Event::Icp(_) | Event::Dip(_) => {
return Err(KelError::InvalidData(
"Multiple inception events in KEL".into(),
));
}
Event::Drt(_) => {
return Err(KelError::InvalidData(
"Delegated rotation not yet supported in KEL replay".into(),
));
}
}
}
Ok(state)
}
fn read_event_from_commit(&self, commit: &Commit) -> Result<Event, KelError> {
let tree = commit.tree()?;
let entry = tree
.get_name(EVENT_BLOB_NAME)
.ok_or_else(|| KelError::InvalidData("Missing event.json in commit".into()))?;
let blob = self.repo.find_blob(entry.id())?;
let event: Event = serde_json::from_slice(blob.content())
.map_err(|e| KelError::Serialization(e.to_string()))?;
Ok(event)
}
fn signature(
&self,
now: chrono::DateTime<chrono::Utc>,
) -> Result<Signature<'static>, KelError> {
self.repo
.signature()
.or_else(|_| {
Signature::new("auths", "auths@local", &git2::Time::new(now.timestamp(), 0))
})
.map_err(KelError::Git)
}
pub fn tip_commit_hash(&self) -> Result<EventHash, KelError> {
let ref_name = &self.ref_path;
let reference = self.repo.find_reference(ref_name).map_err(|e| {
if e.code() == ErrorCode::NotFound {
KelError::NotFound(self.prefix.as_str().to_string())
} else {
KelError::Git(e)
}
})?;
let commit = reference.peel_to_commit()?;
Ok(oid_to_event_hash(commit.id()))
}
pub fn read_event_from_commit_hash(&self, hash: EventHash) -> Result<Event, KelError> {
let commit = self.repo.find_commit(event_hash_to_oid(hash))?;
self.read_event_from_commit(&commit)
}
pub fn parent_hash(&self, hash: EventHash) -> Result<Option<EventHash>, KelError> {
let commit = self.repo.find_commit(event_hash_to_oid(hash))?;
if commit.parent_count() == 0 {
Ok(None)
} else {
Ok(Some(oid_to_event_hash(commit.parent_id(0)?)))
}
}
pub fn parent_count(&self, hash: EventHash) -> Result<usize, KelError> {
let commit = self.repo.find_commit(event_hash_to_oid(hash))?;
Ok(commit.parent_count())
}
pub fn commit_exists(&self, hash: EventHash) -> bool {
self.repo.find_commit(event_hash_to_oid(hash)).is_ok()
}
pub fn parse_hash(hex: &str) -> Result<EventHash, KelError> {
hex.parse::<EventHash>()
.map_err(|e| KelError::InvalidData(format!("Invalid commit hash: {}", e)))
}
}
#[cfg(test)]
#[allow(clippy::disallowed_methods)]
mod tests {
use super::*;
use crate::keri::inception::create_keri_identity_with_curve;
use crate::keri::rotation::rotate_keys;
use crate::keri::{CesrKey, KeriSequence, Prefix, RotEvent, Said, Threshold, VersionString};
use tempfile::TempDir;
fn setup_repo() -> (TempDir, Repository) {
let dir = TempDir::new().unwrap();
let repo = Repository::init(dir.path()).unwrap();
let mut config = repo.config().unwrap();
config.set_str("user.name", "Test User").unwrap();
config.set_str("user.email", "test@example.com").unwrap();
(dir, repo)
}
fn with_temp_auths_home_and_repo<F>(f: F)
where
F: FnOnce(&TempDir, &Repository),
{
let (dir, repo) = setup_repo();
f(&dir, &repo);
}
fn make_icp_event(prefix: &str) -> IcpEvent {
IcpEvent {
v: VersionString::placeholder(),
d: Said::new_unchecked(prefix.to_string()),
i: Prefix::new_unchecked(prefix.to_string()),
s: KeriSequence::new(0),
kt: Threshold::Simple(1),
k: vec![CesrKey::new_unchecked("DKey1".to_string())],
nt: Threshold::Simple(1),
n: vec![Said::new_unchecked("ENext1".to_string())],
bt: Threshold::Simple(0),
b: vec![],
c: vec![],
a: vec![],
}
}
#[test]
fn create_and_read_kel() {
let (_dir, repo) = setup_repo();
let kel = GitKel::new(&repo, "ETest123");
let icp = make_icp_event("ETest123");
kel.create(&icp, chrono::Utc::now()).unwrap();
assert!(kel.exists());
let events = kel.get_events().unwrap();
assert_eq!(events.len(), 1);
assert!(events[0].is_inception());
}
#[test]
fn cannot_create_duplicate_kel() {
let (_dir, repo) = setup_repo();
let kel = GitKel::new(&repo, "ETest123");
let icp = make_icp_event("ETest123");
kel.create(&icp, chrono::Utc::now()).unwrap();
let result = kel.create(&icp, chrono::Utc::now());
assert!(result.is_err());
}
#[test]
fn append_rotation_event() {
let (_dir, repo) = setup_repo();
let init = create_keri_identity_with_curve(
&repo,
None,
chrono::Utc::now(),
auths_crypto::CurveType::Ed25519,
)
.unwrap();
let _rot = rotate_keys(
&repo,
&init.prefix,
&init.next_keypair_pkcs8,
None,
chrono::Utc::now(),
)
.unwrap();
let kel = GitKel::new(&repo, init.prefix.as_str());
let events = kel.get_events().unwrap();
assert_eq!(events.len(), 2);
assert!(events[0].is_inception());
assert!(events[1].is_rotation());
}
#[test]
fn append_rejects_invalid_signature() {
let (_dir, repo) = setup_repo();
let init = create_keri_identity_with_curve(
&repo,
None,
chrono::Utc::now(),
auths_crypto::CurveType::Ed25519,
)
.unwrap();
let kel = GitKel::new(&repo, init.prefix.as_str());
let rot = Event::Rot(RotEvent {
v: VersionString::placeholder(),
d: Said::new_unchecked("EFakeSaid".to_string()),
i: init.prefix.clone(),
s: KeriSequence::new(1),
p: Said::new_unchecked(init.prefix.as_str().to_string()),
kt: Threshold::Simple(1),
k: vec![CesrKey::new_unchecked("DFakeKey".to_string())],
nt: Threshold::Simple(1),
n: vec![Said::new_unchecked("EFakeNext".to_string())],
bt: Threshold::Simple(0),
br: vec![],
ba: vec![],
c: vec![],
a: vec![],
});
let result = kel.append(&rot, chrono::Utc::now());
assert!(result.is_err());
assert!(matches!(result, Err(KelError::ValidationFailed(_))));
}
#[test]
fn get_state_after_inception() {
let (_dir, repo) = setup_repo();
let init = create_keri_identity_with_curve(
&repo,
None,
chrono::Utc::now(),
auths_crypto::CurveType::Ed25519,
)
.unwrap();
let kel = GitKel::new(&repo, init.prefix.as_str());
let state = kel.get_state(chrono::Utc::now()).unwrap();
assert_eq!(state.prefix.as_str(), init.prefix.as_str());
assert_eq!(state.sequence, 0);
assert!(state.can_rotate());
}
#[test]
fn get_state_after_rotation() {
let (_dir, repo) = setup_repo();
let init = create_keri_identity_with_curve(
&repo,
None,
chrono::Utc::now(),
auths_crypto::CurveType::Ed25519,
)
.unwrap();
let rot = rotate_keys(
&repo,
&init.prefix,
&init.next_keypair_pkcs8,
None,
chrono::Utc::now(),
)
.unwrap();
let kel = GitKel::new(&repo, init.prefix.as_str());
let state = kel.get_state(chrono::Utc::now()).unwrap();
assert_eq!(state.sequence, 1);
assert_eq!(rot.sequence, 1);
}
#[test]
fn get_latest_event() {
let (_dir, repo) = setup_repo();
let init = create_keri_identity_with_curve(
&repo,
None,
chrono::Utc::now(),
auths_crypto::CurveType::Ed25519,
)
.unwrap();
let kel = GitKel::new(&repo, init.prefix.as_str());
let latest = kel.get_latest_event().unwrap();
assert!(latest.is_inception());
let _rot = rotate_keys(
&repo,
&init.prefix,
&init.next_keypair_pkcs8,
None,
chrono::Utc::now(),
)
.unwrap();
let latest = kel.get_latest_event().unwrap();
assert!(latest.is_rotation());
}
#[test]
fn not_found_error_for_missing_kel() {
let (_dir, repo) = setup_repo();
let kel = GitKel::new(&repo, "ENotExist");
let result = kel.get_events();
assert!(matches!(result, Err(KelError::NotFound(_))));
}
#[test]
fn cannot_append_icp_event() {
let (_dir, repo) = setup_repo();
let init = create_keri_identity_with_curve(
&repo,
None,
chrono::Utc::now(),
auths_crypto::CurveType::Ed25519,
)
.unwrap();
let kel = GitKel::new(&repo, init.prefix.as_str());
let icp2 = Event::Icp(make_icp_event("EFake"));
let result = kel.append(&icp2, chrono::Utc::now());
assert!(matches!(result, Err(KelError::ValidationFailed(_))));
}
fn make_rot_event(prefix: &str, seq: u128, prev_said: &str) -> RotEvent {
RotEvent {
v: VersionString::placeholder(),
d: Said::new_unchecked(format!("ERot{}", seq)),
i: Prefix::new_unchecked(prefix.to_string()),
s: KeriSequence::new(seq),
p: Said::new_unchecked(prev_said.to_string()),
kt: Threshold::Simple(1),
k: vec![CesrKey::new_unchecked(format!("DKey{}", seq + 1))],
nt: Threshold::Simple(1),
n: vec![Said::new_unchecked(format!("ENext{}", seq + 1))],
bt: Threshold::Simple(0),
br: vec![],
ba: vec![],
c: vec![],
a: vec![],
}
}
#[test]
fn test_cold_cache_full_replay() {
let (_dir, repo) = setup_repo();
let init = create_keri_identity_with_curve(
&repo,
None,
chrono::Utc::now(),
auths_crypto::CurveType::Ed25519,
)
.unwrap();
let kel = GitKel::new(&repo, init.prefix.as_str());
let state = kel.get_state(chrono::Utc::now()).unwrap();
assert_eq!(state.prefix.as_str(), init.prefix.as_str());
assert_eq!(state.sequence, 0);
let did = format!("did:keri:{}", init.prefix.as_str());
let tip_said = kel.get_latest_event().unwrap().said().to_string();
let cached = cache::try_load_cached_state(_dir.path(), &did, &tip_said);
assert!(cached.is_some());
}
#[test]
fn test_warm_cache_hit() {
let (_dir, repo) = setup_repo();
let init = create_keri_identity_with_curve(
&repo,
None,
chrono::Utc::now(),
auths_crypto::CurveType::Ed25519,
)
.unwrap();
let kel = GitKel::new(&repo, init.prefix.as_str());
let state1 = kel.get_state(chrono::Utc::now()).unwrap();
let state2 = kel.get_state(chrono::Utc::now()).unwrap();
assert_eq!(state1, state2);
assert_eq!(state2.sequence, 0);
}
#[test]
fn test_incremental_validation_after_rotation() {
let (_dir, repo) = setup_repo();
let init = create_keri_identity_with_curve(
&repo,
None,
chrono::Utc::now(),
auths_crypto::CurveType::Ed25519,
)
.unwrap();
let kel = GitKel::new(&repo, init.prefix.as_str());
let _ = kel.get_state(chrono::Utc::now()).unwrap();
let rot1 = rotate_keys(
&repo,
&init.prefix,
&init.next_keypair_pkcs8,
None,
chrono::Utc::now(),
)
.unwrap();
let state = kel.get_state(chrono::Utc::now()).unwrap();
assert_eq!(state.sequence, 1);
let _rot2 = rotate_keys(
&repo,
&init.prefix,
&rot1.new_next_keypair_pkcs8,
None,
chrono::Utc::now(),
)
.unwrap();
let state = kel.get_state(chrono::Utc::now()).unwrap();
assert_eq!(state.sequence, 2);
}
#[test]
fn test_cache_divergence_fallback() {
let (_dir, repo) = setup_repo();
let init = create_keri_identity_with_curve(
&repo,
None,
chrono::Utc::now(),
auths_crypto::CurveType::Ed25519,
)
.unwrap();
let kel = GitKel::new(&repo, init.prefix.as_str());
let _ = kel.get_state(chrono::Utc::now()).unwrap();
let did = format!("did:keri:{}", init.prefix.as_str());
let cached_full = cache::try_load_cached_state_full(_dir.path(), &did).unwrap();
let _ = cache::write_kel_cache(
_dir.path(),
&did,
&cached_full.state,
cached_full.validated_against_tip_said.as_str(),
"0000000000000000000000000000000000000000",
chrono::Utc::now(),
);
let state = kel.get_state(chrono::Utc::now()).unwrap();
assert_eq!(state.prefix.as_str(), init.prefix.as_str());
}
#[test]
fn test_get_state_matches_full_replay() {
let (_dir, repo) = setup_repo();
let init = create_keri_identity_with_curve(
&repo,
None,
chrono::Utc::now(),
auths_crypto::CurveType::Ed25519,
)
.unwrap();
let rot1 = rotate_keys(
&repo,
&init.prefix,
&init.next_keypair_pkcs8,
None,
chrono::Utc::now(),
)
.unwrap();
let _rot2 = rotate_keys(
&repo,
&init.prefix,
&rot1.new_next_keypair_pkcs8,
None,
chrono::Utc::now(),
)
.unwrap();
let kel = GitKel::new(&repo, init.prefix.as_str());
let state_incremental = kel.get_state(chrono::Utc::now()).unwrap();
let did = format!("did:keri:{}", init.prefix.as_str());
let _ = cache::invalidate_cache(_dir.path(), &did);
let state_full = kel.get_state_full_replay(chrono::Utc::now()).unwrap();
assert_eq!(state_incremental.prefix, state_full.prefix);
assert_eq!(state_incremental.sequence, state_full.sequence);
assert_eq!(state_incremental.current_keys, state_full.current_keys);
assert_eq!(
state_incremental.last_event_said,
state_full.last_event_said
);
}
#[test]
fn test_cache_said_mismatch_forces_replay() {
let (_dir, repo) = setup_repo();
let init = create_keri_identity_with_curve(
&repo,
None,
chrono::Utc::now(),
auths_crypto::CurveType::Ed25519,
)
.unwrap();
let kel = GitKel::new(&repo, init.prefix.as_str());
let _ = kel.get_state(chrono::Utc::now()).unwrap();
let did = format!("did:keri:{}", init.prefix.as_str());
let cached_full = cache::try_load_cached_state_full(_dir.path(), &did).unwrap();
let _ = cache::write_kel_cache(
_dir.path(),
&did,
&cached_full.state,
"EFakeSaidThatDoesNotMatchCommit",
cached_full.last_commit_oid.as_str(),
chrono::Utc::now(),
);
let state = kel.get_state(chrono::Utc::now()).unwrap();
assert_eq!(state.prefix.as_str(), init.prefix.as_str());
assert_eq!(state.sequence, 0);
let new_cached = cache::try_load_cached_state_full(_dir.path(), &did).unwrap();
let tip_said = kel.get_latest_event().unwrap().said().to_string();
assert_eq!(
new_cached.validated_against_tip_said.as_str(),
tip_said.as_str()
);
}
#[test]
fn test_commit_hash_helpers() {
let (_dir, repo) = setup_repo();
let init = create_keri_identity_with_curve(
&repo,
None,
chrono::Utc::now(),
auths_crypto::CurveType::Ed25519,
)
.unwrap();
let kel = GitKel::new(&repo, init.prefix.as_str());
let tip_hash = kel.tip_commit_hash().unwrap();
assert!(kel.commit_exists(tip_hash));
let event = kel.read_event_from_commit_hash(tip_hash).unwrap();
assert!(event.is_inception());
assert!(kel.parent_hash(tip_hash).unwrap().is_none());
let _rot = rotate_keys(
&repo,
&init.prefix,
&init.next_keypair_pkcs8,
None,
chrono::Utc::now(),
)
.unwrap();
let new_tip = kel.tip_commit_hash().unwrap();
assert_ne!(tip_hash, new_tip);
let parent = kel.parent_hash(new_tip).unwrap();
assert!(parent.is_some());
assert_eq!(parent.unwrap(), tip_hash);
}
#[test]
fn test_kel_merge_commit_rejected() {
with_temp_auths_home_and_repo(|_repo_dir, repo| {
let prefix = "EMergeReject";
let kel = GitKel::new(repo, prefix);
let icp = make_icp_event(prefix);
kel.create(&icp, chrono::Utc::now()).unwrap();
let _ = kel.get_state(chrono::Utc::now()).unwrap();
let inception_hash = kel.tip_commit_hash().unwrap();
let inception_oid = crate::witness::event_hash_to_oid(inception_hash);
let rot1 = Event::Rot(make_rot_event(prefix, 1, prefix));
let rot1_json = serde_json::to_vec_pretty(&rot1).unwrap();
let blob1_oid = repo.blob(&rot1_json).unwrap();
let mut tb1 = repo.treebuilder(None).unwrap();
tb1.insert("event.json", blob1_oid, 0o100644).unwrap();
let tree1_oid = tb1.write().unwrap();
let tree1 = repo.find_tree(tree1_oid).unwrap();
let inception_commit = repo.find_commit(inception_oid).unwrap();
let sig = repo.signature().unwrap();
let branch1_oid = repo
.commit(None, &sig, &sig, "Branch 1", &tree1, &[&inception_commit])
.unwrap();
let rot2 = Event::Rot(make_rot_event(prefix, 1, prefix));
let rot2_json = serde_json::to_vec_pretty(&rot2).unwrap();
let blob2_oid = repo.blob(&rot2_json).unwrap();
let mut tb2 = repo.treebuilder(None).unwrap();
tb2.insert("event.json", blob2_oid, 0o100644).unwrap();
let tree2_oid = tb2.write().unwrap();
let tree2 = repo.find_tree(tree2_oid).unwrap();
let branch2_oid = repo
.commit(None, &sig, &sig, "Branch 2", &tree2, &[&inception_commit])
.unwrap();
let branch1_commit = repo.find_commit(branch1_oid).unwrap();
let branch2_commit = repo.find_commit(branch2_oid).unwrap();
let merge_oid = repo
.commit(
None,
&sig,
&sig,
"Merge (invalid)",
&tree1,
&[&branch1_commit, &branch2_commit],
)
.unwrap();
let ref_name = format!("refs/did/keri/{}/kel", prefix);
repo.reference(&ref_name, merge_oid, true, "Force merge commit")
.unwrap();
let result = kel.get_state(chrono::Utc::now());
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
matches!(err, KelError::ChainIntegrity(_)),
"Expected ChainIntegrity error, got: {:?}",
err
);
let msg = err.to_string();
assert!(
msg.contains("non-linear"),
"Error message should mention non-linear: {}",
msg
);
});
}
}