use std::sync::Arc;
use std::time::Duration;
use affinidi_messaging_didcomm_service::{
DIDCommServiceConfig, ListenerConfig, Protocols, RestartPolicy, RetryConfig,
};
use affinidi_tdk_common::profiles::TDKProfile;
use tracing::{info, warn};
use super::identity::{IdentityGeneration, ServiceIdentity};
const LIVENESS_POLL: Duration = Duration::from_secs(15);
pub fn needs_drain(identity: &ServiceIdentity, generation: &IdentityGeneration) -> bool {
generation.retired_at.is_some()
&& generation.mediator_did.is_some()
&& generation.mediator_did != identity.current().mediator_did
}
pub fn generations_needing_drain(identity: &ServiceIdentity) -> Vec<IdentityGeneration> {
identity
.generations()
.into_iter()
.filter(|g| needs_drain(identity, g))
.collect()
}
pub fn drain_listener_config(
alias: &str,
identity: &ServiceIdentity,
generation: &IdentityGeneration,
) -> Option<ListenerConfig> {
let mediator = generation.mediator_did.as_deref()?;
let profile = TDKProfile::new(
&format!("{alias}-drain-{}", generation.id),
&identity.did,
Some(mediator),
identity.secrets(),
);
let protocols = match (generation.protocols.didcomm, generation.protocols.tsp) {
(true, true) => Protocols::BOTH,
(false, true) => Protocols::TSP_ONLY,
_ => Protocols::DIDCOMM_ONLY,
};
Some(ListenerConfig {
id: format!("{alias}-drain-{}", generation.id),
profile,
restart_policy: RestartPolicy::Always {
backoff: RetryConfig::default(),
},
auto_delete: true,
protocols,
..Default::default()
})
}
pub fn drain_service_config(listener: ListenerConfig) -> DIDCommServiceConfig {
DIDCommServiceConfig {
listeners: vec![listener],
}
}
pub async fn wait_until_generation_retires(identity: Arc<ServiceIdentity>, generation_id: u64) {
let mut timer = tokio::time::interval(LIVENESS_POLL);
timer.tick().await;
loop {
timer.tick().await;
if !identity.generations().iter().any(|g| g.id == generation_id) {
info!(
generation = generation_id,
"retired generation is no longer live — stopping its mediator drain"
);
return;
}
}
}
pub fn warn_drain_failed(generation: &IdentityGeneration, error: &str) {
warn!(
generation = generation.id,
mediator = ?generation.mediator_did,
"could not connect to the old mediator to drain it: {error}. \
Messages queued there by peers holding a stale DID document will not be \
delivered. If the DID has been deregistered from that mediator, this \
cannot be recovered."
);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::server::identity::ProtocolSet;
const DID: &str = "did:webvh:example:alpha";
const MEDIATOR_A: &str = "did:web:mediator-a.example";
const MEDIATOR_B: &str = "did:web:mediator-b.example";
fn generation(id: u64, mediator: Option<&str>, retired: bool) -> IdentityGeneration {
IdentityGeneration {
id,
did: DID.into(),
signing_kid: format!("{DID}#z6Mk{id}"),
ka_kid: format!("{DID}#z6LS{id}"),
ka_public_multibase: None,
mediator_did: mediator.map(str::to_string),
protocols: ProtocolSet {
didcomm: true,
tsp: false,
},
created_at: 100,
retired_at: retired.then_some(200),
expires_at: retired.then_some(4_000),
}
}
#[tokio::test]
async fn a_same_mediator_key_rotation_needs_no_drain() {
let current = generation(1, Some(MEDIATOR_A), false);
let retired = generation(0, Some(MEDIATOR_A), true);
let identity = ServiceIdentity::for_test(DID, vec![current, retired.clone()]).await;
assert!(!needs_drain(&identity, &retired));
assert!(generations_needing_drain(&identity).is_empty());
}
#[tokio::test]
async fn a_mediator_change_needs_a_drain() {
let current = generation(1, Some(MEDIATOR_B), false);
let retired = generation(0, Some(MEDIATOR_A), true);
let identity = ServiceIdentity::for_test(DID, vec![current, retired.clone()]).await;
assert!(needs_drain(&identity, &retired));
let needing = generations_needing_drain(&identity);
assert_eq!(needing.len(), 1);
assert_eq!(
needing[0].id, 0,
"the retired generation, on the old mediator"
);
}
#[tokio::test]
async fn the_current_generation_is_never_drained() {
let current = generation(1, Some(MEDIATOR_B), false);
let identity = ServiceIdentity::for_test(DID, vec![current.clone()]).await;
assert!(!needs_drain(&identity, ¤t));
assert!(generations_needing_drain(&identity).is_empty());
}
#[tokio::test]
async fn a_generation_with_no_mediator_is_not_drained() {
let current = generation(1, Some(MEDIATOR_B), false);
let retired = generation(0, None, true);
let identity = ServiceIdentity::for_test(DID, vec![current, retired.clone()]).await;
assert!(!needs_drain(&identity, &retired));
assert!(drain_listener_config("control", &identity, &retired).is_none());
}
#[tokio::test]
async fn the_drain_listener_points_at_the_old_mediator() {
let current = generation(1, Some(MEDIATOR_B), false);
let retired = generation(0, Some(MEDIATOR_A), true);
let identity = ServiceIdentity::for_test(DID, vec![current, retired.clone()]).await;
let listener = drain_listener_config("control", &identity, &retired).expect("drain config");
assert_eq!(listener.profile.mediator.as_deref(), Some(MEDIATOR_A));
assert_eq!(listener.profile.did, DID);
assert_eq!(
listener.id, "control-drain-0",
"generation-scoped so it can never collide with the main listener"
);
}
}