use auths_id::keri::{
Event, KelResolveError, KelResolver, LocalKelResolver, Prefix, parse_did_keri,
verify_prefix_binding,
};
use auths_id::ports::registry::RegistryBackend;
use auths_storage::git::{RemoteKelError, RemoteKelSource};
use auths_verifier::duplicity::{DuplicityReport, KelEventRef, detect_duplicity};
pub struct KelResolverChain<'a> {
registry: &'a dyn RegistryBackend,
remote: Option<RemoteKelSource>,
}
impl<'a> KelResolverChain<'a> {
pub fn local(registry: &'a dyn RegistryBackend) -> Self {
Self {
registry,
remote: None,
}
}
pub fn with_remote(registry: &'a dyn RegistryBackend, remote_url: impl Into<String>) -> Self {
Self {
registry,
remote: Some(RemoteKelSource::new(remote_url)),
}
}
pub fn resolve_kel(&self, did: &str) -> Result<Vec<Event>, KelResolveError> {
let local = LocalKelResolver::new(self.registry).resolve_kel(did);
match &self.remote {
None => local,
Some(remote) => self.reconcile(did, local, remote),
}
}
fn reconcile(
&self,
did: &str,
local: Result<Vec<Event>, KelResolveError>,
remote: &RemoteKelSource,
) -> Result<Vec<Event>, KelResolveError> {
let prefix =
parse_did_keri(did).map_err(|_| KelResolveError::InvalidDid(did.to_string()))?;
let remote = fetch_remote_guarded(remote, &prefix);
match (local, remote) {
(Ok(local_kel), Ok(remote_kel)) => {
if let Some((sequence, saids)) = cross_source_fork(&prefix, &local_kel, &remote_kel)
{
return Err(KelResolveError::Diverging { sequence, saids });
}
choose_newer_no_rollback(local_kel, remote_kel)
}
(Ok(local_kel), Err(_)) => Ok(local_kel),
(Err(KelResolveError::NotFound(_)), Ok(remote_kel)) => Ok(remote_kel),
(Err(local_err), Ok(_)) => Err(local_err),
(Err(KelResolveError::NotFound(_)), Err(remote_err)) => Err(remote_err),
(Err(local_err), Err(_)) => Err(local_err),
}
}
}
fn fetch_remote_guarded(
remote: &RemoteKelSource,
prefix: &Prefix,
) -> Result<Vec<Event>, KelResolveError> {
let events = remote.fetch_kel(prefix).map_err(map_remote_err)?;
verify_prefix_binding(prefix, &events)?;
Ok(events)
}
fn choose_newer_no_rollback(
local_kel: Vec<Event>,
remote_kel: Vec<Event>,
) -> Result<Vec<Event>, KelResolveError> {
let local_tip = tip_seq(&local_kel);
let remote_tip = tip_seq(&remote_kel);
if remote_tip < local_tip {
return Err(KelResolveError::Rollback {
local_tip,
remote_tip,
});
}
Ok(if remote_tip > local_tip {
remote_kel
} else {
local_kel
})
}
fn tip_seq(events: &[Event]) -> u128 {
events.last().map(|e| e.sequence().value()).unwrap_or(0)
}
fn cross_source_fork(
prefix: &Prefix,
local: &[Event],
remote: &[Event],
) -> Option<(u128, Vec<String>)> {
let refs: Vec<KelEventRef> = local
.iter()
.chain(remote.iter())
.map(|e| KelEventRef {
prefix: prefix.as_str(),
seq: e.sequence().value() as u64,
said: e.said().as_str(),
})
.collect();
match detect_duplicity(&refs) {
DuplicityReport::Clean => None,
DuplicityReport::Diverging {
seq, event_saids, ..
} => Some((seq as u128, event_saids)),
}
}
fn map_remote_err(err: RemoteKelError) -> KelResolveError {
match err {
RemoteKelError::Fetch { url, source } => {
KelResolveError::Network(format!("{url}: {source}"))
}
RemoteKelError::Setup(e) => KelResolveError::Network(e.to_string()),
RemoteKelError::NoRegistry { reference } => {
KelResolveError::NotFound(format!("remote has no {reference}"))
}
RemoteKelError::NotFound(s) => KelResolveError::NotFound(s),
RemoteKelError::Oversized { what } => KelResolveError::Oversized(what),
RemoteKelError::Truncated => KelResolveError::Truncated,
RemoteKelError::Read(e) => KelResolveError::Backend(e.to_string()),
RemoteKelError::Serialize(s) => KelResolveError::Replay(s),
other => KelResolveError::Backend(other.to_string()),
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use auths_id::testing::fakes::FakeRegistryBackend;
use auths_keri::{
CesrKey, IcpEvent, IxnEvent, KeriPublicKey, KeriSequence, Said, Seal, Threshold,
VersionString, compute_next_commitment, finalize_icp_event, finalize_ixn_event,
};
use auths_storage::git::{GitRegistryBackend, RegistryConfig};
use tempfile::TempDir;
fn icp_and_prefix(seed: u8) -> (Event, Prefix) {
let key = KeriPublicKey::ed25519(&[seed; 32]).unwrap();
let next = KeriPublicKey::ed25519(&[seed.wrapping_add(1); 32]).unwrap();
let icp = IcpEvent {
v: VersionString::placeholder(),
d: Said::default(),
i: Prefix::default(),
s: KeriSequence::new(0),
kt: Threshold::Simple(1),
k: vec![CesrKey::new_unchecked(key.to_qb64().unwrap())],
nt: Threshold::Simple(1),
n: vec![compute_next_commitment(&next)],
bt: Threshold::Simple(0),
b: vec![],
c: vec![],
a: vec![],
};
let finalized = finalize_icp_event(icp).unwrap();
let prefix = finalized.i.clone();
(Event::Icp(finalized), prefix)
}
fn ixn_at(prefix: &Prefix, seq: u128, prev: &Said) -> Event {
let ixn = IxnEvent {
v: VersionString::placeholder(),
d: Said::default(),
i: prefix.clone(),
s: KeriSequence::new(seq),
p: prev.clone(),
a: vec![],
};
Event::Ixn(finalize_ixn_event(ixn).unwrap())
}
#[test]
fn local_only_resolves_from_local() {
let (event, prefix) = icp_and_prefix(1);
let did = format!("did:keri:{prefix}");
let registry = FakeRegistryBackend::new();
registry.append_event(&prefix, &event).unwrap();
let chain = KelResolverChain::local(®istry);
assert_eq!(chain.resolve_kel(&did).unwrap(), vec![event]);
}
#[test]
fn local_only_unknown_is_not_found() {
let registry = FakeRegistryBackend::new();
let chain = KelResolverChain::local(®istry);
let err = chain
.resolve_kel("did:keri:ENotHere0000000000000000000000000000000000")
.unwrap_err();
assert!(matches!(err, KelResolveError::NotFound(_)));
}
#[test]
fn remote_fallback_resolves_with_no_local_seeding() {
let src = TempDir::new().unwrap();
let source =
GitRegistryBackend::from_config_unchecked(RegistryConfig::single_tenant(src.path()));
source.init_if_needed().unwrap();
let (event, prefix) = icp_and_prefix(2);
source.append_event(&prefix, &event).unwrap();
let url = format!("file://{}", src.path().display());
let local = FakeRegistryBackend::new();
let chain = KelResolverChain::with_remote(&local, url);
let did = format!("did:keri:{prefix}");
assert_eq!(chain.resolve_kel(&did).unwrap(), vec![event]);
}
#[test]
fn monotonicity_rejects_older_remote() {
let (icp, prefix) = icp_and_prefix(3);
let icp_said = match &icp {
Event::Icp(e) => e.d.clone(),
_ => unreachable!(),
};
let local_kel = vec![icp.clone(), ixn_at(&prefix, 1, &icp_said)]; let remote_kel = vec![icp];
let err = choose_newer_no_rollback(local_kel, remote_kel).unwrap_err();
assert!(matches!(err, KelResolveError::Rollback { .. }));
}
#[test]
fn monotonicity_accepts_strictly_newer_remote() {
let (icp, prefix) = icp_and_prefix(4);
let icp_said = match &icp {
Event::Icp(e) => e.d.clone(),
_ => unreachable!(),
};
let local_kel = vec![icp.clone()]; let newer = vec![icp, ixn_at(&prefix, 1, &icp_said)];
let chosen = choose_newer_no_rollback(local_kel, newer.clone()).unwrap();
assert_eq!(chosen, newer);
}
#[test]
fn equal_tip_prefers_local() {
let (icp, _prefix) = icp_and_prefix(5);
let local_kel = vec![icp.clone()];
let remote_kel = vec![icp];
let chosen = choose_newer_no_rollback(local_kel.clone(), remote_kel).unwrap();
assert_eq!(chosen, local_kel);
}
#[test]
fn remote_error_maps_into_taxonomy() {
assert!(matches!(
map_remote_err(RemoteKelError::Truncated),
KelResolveError::Truncated
));
assert!(matches!(
map_remote_err(RemoteKelError::NotFound("x".into())),
KelResolveError::NotFound(_)
));
assert!(matches!(
map_remote_err(RemoteKelError::Oversized { what: "big".into() }),
KelResolveError::Oversized(_)
));
}
fn conflicting_ixn_at(prefix: &Prefix, seq: u128, prev: &Said) -> Event {
let ixn = IxnEvent {
v: VersionString::placeholder(),
d: Said::default(),
i: prefix.clone(),
s: KeriSequence::new(seq),
p: prev.clone(),
a: vec![Seal::digest("EConflictingAnchor")],
};
Event::Ixn(finalize_ixn_event(ixn).unwrap())
}
#[test]
fn cross_source_fork_flagged() {
let (icp, prefix) = icp_and_prefix(7);
let icp_said = match &icp {
Event::Icp(e) => e.d.clone(),
_ => unreachable!(),
};
let local = vec![icp.clone(), ixn_at(&prefix, 1, &icp_said)];
let remote = vec![icp, conflicting_ixn_at(&prefix, 1, &icp_said)];
let fork = cross_source_fork(&prefix, &local, &remote);
assert!(fork.is_some(), "a same-seq SAID mismatch must be flagged");
let (seq, saids) = fork.unwrap();
assert_eq!(seq, 1);
assert_eq!(saids.len(), 2);
}
#[test]
fn clean_multi_source_resolves() {
let (icp, prefix) = icp_and_prefix(8);
let icp_said = match &icp {
Event::Icp(e) => e.d.clone(),
_ => unreachable!(),
};
let local = vec![icp.clone(), ixn_at(&prefix, 1, &icp_said)];
let remote = local.clone(); assert!(cross_source_fork(&prefix, &local, &remote).is_none());
}
#[test]
fn first_seen_retained_without_conflict() {
let (icp, prefix) = icp_and_prefix(9);
let local = vec![icp.clone()];
let remote = vec![icp];
assert!(cross_source_fork(&prefix, &local, &remote).is_none());
let chosen = choose_newer_no_rollback(local.clone(), remote).unwrap();
assert_eq!(chosen, local);
}
}