use std::sync::Arc;
use thiserror::Error;
use tokio::sync::RwLock;
use tracing::info;
use crate::auth::AuthClaims;
use crate::config::AppConfig;
use crate::error::AppError;
use crate::operations::did_webvh::UpdateDidWebvhError;
use crate::operations::protocol::disable_tsp::{DisableTspError, DisableTspParams, disable_tsp};
use crate::operations::protocol::document::{DocumentPatchError, current_tsp_service};
use crate::operations::protocol::enable_tsp::{EnableTspError, EnableTspParams, enable_tsp};
use crate::operations::protocol::snapshot::{
self, ServiceConfigSnapshot, ServiceKind, TspSnapshot,
};
use crate::operations::protocol::update_tsp::{UpdateTspError, UpdateTspParams, update_tsp};
use crate::operations::protocol::{OpContext, ServiceOpDeps};
use crate::store::KeyspaceHandle;
#[derive(Debug, Clone, Default)]
pub struct RollbackTspParams;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RollbackKind {
Disabled,
Enabled,
Updated,
NoOp,
}
#[derive(Debug, Clone)]
pub struct RollbackTspResult {
pub new_version_id: Option<String>,
pub kind: RollbackKind,
pub vta_did: String,
pub serverless: bool,
}
#[derive(Debug, Error)]
pub enum RollbackTspError {
#[error(
"no prior mutation for `services tsp` to roll back from. \
Use `services tsp enable / update / disable` directly instead."
)]
NoPriorMutation,
#[error("VTA DID is not configured — run `vta setup` first")]
VtaDidNotConfigured,
#[error("VTA DID `{0}` has no webvh record")]
VtaDidRecordMissing(String),
#[error("VTA DID `{0}` has no published log")]
VtaDidLogMissing(String),
#[error("VTA DID log is empty")]
EmptyLog,
#[error(transparent)]
EnableForward(#[from] EnableTspError),
#[error(transparent)]
UpdateForward(#[from] UpdateTspError),
#[error(transparent)]
DisableForward(#[from] DisableTspError),
#[error("DID document patch failed: {0}")]
DocumentPatch(#[from] DocumentPatchError),
#[error("WebVH update failed: {0}")]
WebVHUpdate(#[from] UpdateDidWebvhError),
#[error("auth: {0}")]
Auth(String),
#[error("storage error: {0}")]
Storage(String),
}
impl From<AppError> for RollbackTspError {
fn from(value: AppError) -> Self {
Self::Storage(value.to_string())
}
}
impl From<crate::operations::protocol::preconditions::ProtocolPreconditionError>
for RollbackTspError
{
fn from(value: crate::operations::protocol::preconditions::ProtocolPreconditionError) -> Self {
use crate::operations::protocol::preconditions::ProtocolPreconditionError as E;
match value {
E::VtaDidNotConfigured => Self::VtaDidNotConfigured,
E::VtaDidRecordMissing(s) => Self::VtaDidRecordMissing(s),
E::VtaDidLogMissing(s) => Self::VtaDidLogMissing(s),
E::EmptyLog => Self::EmptyLog,
E::Storage(s) | E::DocumentParse(s) => Self::Storage(s),
}
}
}
pub async fn rollback_tsp(
deps: &ServiceOpDeps<'_>,
auth: &AuthClaims,
_params: RollbackTspParams,
channel: &str,
) -> Result<RollbackTspResult, RollbackTspError> {
auth.require_super_admin()
.map_err(|e| RollbackTspError::Auth(e.to_string()))?;
let snap = snapshot::read(deps.snapshot_ks, ServiceKind::Tsp)
.await
.map_err(|e| RollbackTspError::Storage(format!("snapshot read: {e}")))?
.ok_or(RollbackTspError::NoPriorMutation)?;
let tsp_snap = match snap {
ServiceConfigSnapshot::Tsp(s) => s,
other => {
return Err(RollbackTspError::Storage(format!(
"snapshot kind mismatch: stored {other:?}, requested Tsp",
)));
}
};
let current_mediator = read_current_tsp_mediator(deps.config, deps.webvh_ks).await?;
info!(
channel,
snapshot = ?tsp_snap,
current = ?current_mediator,
"rollback_tsp dispatching",
);
match (tsp_snap, current_mediator.as_deref()) {
(TspSnapshot::Disabled, Some(_)) => {
let result =
disable_tsp(deps, auth, DisableTspParams, OpContext::Rollback, channel).await?;
Ok(RollbackTspResult {
new_version_id: Some(result.new_version_id),
kind: RollbackKind::Disabled,
vta_did: result.vta_did,
serverless: result.serverless,
})
}
(TspSnapshot::Enabled { mediator_did }, None) => {
let result = enable_tsp(
deps,
auth,
EnableTspParams {
mediator_did: mediator_did.clone(),
},
OpContext::Rollback,
channel,
)
.await?;
Ok(RollbackTspResult {
new_version_id: Some(result.new_version_id),
kind: RollbackKind::Enabled,
vta_did: result.vta_did,
serverless: result.serverless,
})
}
(TspSnapshot::Enabled { mediator_did }, Some(current)) if mediator_did != current => {
let result = update_tsp(
deps,
auth,
UpdateTspParams {
mediator_did: mediator_did.clone(),
},
OpContext::Rollback,
channel,
)
.await?;
Ok(RollbackTspResult {
new_version_id: Some(result.new_version_id),
kind: RollbackKind::Updated,
vta_did: result.vta_did,
serverless: result.serverless,
})
}
_ => {
info!(
channel,
"rollback_tsp: snapshot matches current state — no-op"
);
Ok(RollbackTspResult {
new_version_id: None,
kind: RollbackKind::NoOp,
vta_did: String::new(),
serverless: false,
})
}
}
}
async fn read_current_tsp_mediator(
config: &Arc<RwLock<AppConfig>>,
webvh_ks: &KeyspaceHandle,
) -> Result<Option<String>, RollbackTspError> {
let state = super::preconditions::load_vta_doc_state(config, webvh_ks).await?;
Ok(current_tsp_service(&state.current_doc).map(|svc| svc.mediator_did))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::store::Store;
use vti_common::config::StoreConfig as VtiStoreConfig;
struct TestFixture {
_dir: tempfile::TempDir,
_config: Arc<RwLock<AppConfig>>,
store: Store,
}
impl TestFixture {
fn snapshot_ks(&self) -> KeyspaceHandle {
self.store.keyspace(snapshot::KEYSPACE_NAME).unwrap()
}
}
fn build_fixture(tsp: bool, didcomm: bool) -> TestFixture {
use crate::test_support::test_app_config;
let dir = tempfile::tempdir().unwrap();
let mut cfg = test_app_config(dir.path().into());
cfg.services.tsp = tsp;
cfg.services.didcomm = didcomm;
cfg.vta_did = Some("did:webvh:scid123:host:vta".into());
cfg.config_path = dir.path().join("vta.toml");
let initial = toml::to_string_pretty(&cfg).unwrap();
std::fs::write(&cfg.config_path, initial).unwrap();
let store = Store::open(&VtiStoreConfig {
data_dir: dir.path().into(),
})
.unwrap();
TestFixture {
_dir: dir,
_config: Arc::new(RwLock::new(cfg)),
store,
}
}
#[tokio::test]
async fn no_prior_mutation_when_snapshot_empty() {
let fx = build_fixture(true, true);
let snapshot_ks = fx.snapshot_ks();
let snap = snapshot::read(&snapshot_ks, ServiceKind::Tsp)
.await
.unwrap();
assert!(snap.is_none());
let err = RollbackTspError::NoPriorMutation;
let msg = err.to_string();
assert!(msg.contains("no prior mutation"));
}
#[tokio::test]
async fn snapshot_disabled_round_trips() {
let fx = build_fixture(true, true);
let snapshot_ks = fx.snapshot_ks();
snapshot::write(
&snapshot_ks,
ServiceConfigSnapshot::Tsp(TspSnapshot::Disabled),
)
.await
.unwrap();
let read = snapshot::read(&snapshot_ks, ServiceKind::Tsp)
.await
.unwrap()
.unwrap();
match read {
ServiceConfigSnapshot::Tsp(TspSnapshot::Disabled) => {}
other => panic!("expected Tsp(Disabled), got {other:?}"),
}
}
#[tokio::test]
async fn snapshot_enabled_with_mediator_round_trips() {
let fx = build_fixture(true, true);
let snapshot_ks = fx.snapshot_ks();
snapshot::write(
&snapshot_ks,
ServiceConfigSnapshot::Tsp(TspSnapshot::Enabled {
mediator_did: "did:webvh:scid:host:prior-mediator".into(),
}),
)
.await
.unwrap();
let read = snapshot::read(&snapshot_ks, ServiceKind::Tsp)
.await
.unwrap()
.unwrap();
match read {
ServiceConfigSnapshot::Tsp(TspSnapshot::Enabled { mediator_did }) => {
assert_eq!(mediator_did, "did:webvh:scid:host:prior-mediator");
}
other => panic!("expected Tsp(Enabled {{ mediator_did }}), got {other:?}"),
}
}
#[test]
fn rollback_kind_variants_are_distinct() {
assert_ne!(RollbackKind::Disabled, RollbackKind::Enabled);
assert_ne!(RollbackKind::Enabled, RollbackKind::Updated);
assert_ne!(RollbackKind::Updated, RollbackKind::NoOp);
assert_ne!(RollbackKind::NoOp, RollbackKind::Disabled);
}
}