use serde::{Deserialize, Serialize};
use serde_json::{Map, Value, json};
use crate::policy::effects::{Effect, StatePin};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdatePlan {
pub did: String,
pub scid: String,
pub prior_version_id: String,
pub new_version_id: String,
pub prior_document: Value,
pub new_document: Value,
pub prior_update_keys: Vec<String>,
pub new_update_keys: Vec<String>,
pub pre_rotation_count: u32,
pub new_next_key_hashes: Vec<String>,
pub base_path: String,
pub path_counter_pin: u32,
pub subject_context: String,
pub requester_authorized: bool,
}
impl UpdatePlan {
pub fn state_pin(&self) -> StatePin {
StatePin {
resource: self.did.clone(),
version: self.prior_version_id.clone(),
}
}
pub fn rotates_update_keys(&self) -> bool {
self.new_update_keys != self.prior_update_keys
}
pub fn to_effects(&self) -> Vec<Effect> {
let mut effects = Vec::new();
for change in document_changes(&self.prior_document, &self.new_document) {
effects.push(change);
}
if self.rotates_update_keys() {
effects.push(
Effect::new(
"keyRotation",
"Rotates this DID's update key. A change to the document rotates it — the \
current update key stops being able to authorize further changes.",
)
.before(json!(self.prior_update_keys))
.after(json!(self.new_update_keys)),
);
}
if self.pre_rotation_count > 0 {
let mut detail = Map::new();
detail.insert("commitments".into(), json!(self.pre_rotation_count));
let plural = if self.pre_rotation_count == 1 {
""
} else {
"s"
};
effects.push(
Effect::new(
"preRotationRefresh",
format!(
"Publishes {} fresh pre-rotation commitment{plural}, which will authorize \
the next rotation.",
self.pre_rotation_count
),
)
.detail(detail),
);
}
effects
}
}
fn document_changes(prior: &Value, next: &Value) -> Vec<Effect> {
let (Some(prior), Some(next)) = (prior.as_object(), next.as_object()) else {
if prior != next {
return vec![
Effect::new("documentChange", "Replaces the DID document.")
.before(prior.clone())
.after(next.clone()),
];
}
return vec![];
};
let mut keys: Vec<&String> = prior.keys().chain(next.keys()).collect();
keys.sort();
keys.dedup();
let mut effects = Vec::new();
for key in keys {
let before = prior.get(key);
let after = next.get(key);
if before == after {
continue;
}
let summary = match (before, after) {
(None, Some(_)) => format!("Adds `{key}` to the DID document."),
(Some(_), None) => format!("Removes `{key}` from the DID document."),
_ => format!("Changes `{key}` in the DID document."),
};
let mut effect = Effect::new("documentChange", summary).at(format!("/{key}"));
if let Some(b) = before {
effect = effect.before(b.clone());
}
if let Some(a) = after {
effect = effect.after(a.clone());
}
effects.push(effect);
}
effects
}
#[cfg(test)]
mod tests {
use super::*;
fn plan() -> UpdatePlan {
UpdatePlan {
did: "did:webvh:QmScid:example.com:acme".into(),
scid: "QmScid".into(),
prior_version_id: "3-QmPrior".into(),
new_version_id: "4-QmNext".into(),
prior_document: json!({ "id": "did:webvh:QmScid:example.com:acme" }),
new_document: json!({
"id": "did:webvh:QmScid:example.com:acme",
"service": [{ "id": "#files", "type": "FileStore" }]
}),
prior_update_keys: vec!["z6MkOld".into()],
new_update_keys: vec!["z6MkNew".into()],
pre_rotation_count: 2,
new_next_key_hashes: vec!["QmHashA".into(), "QmHashB".into()],
base_path: "m/1'/2'".into(),
path_counter_pin: 7,
subject_context: "ctx-test".into(),
requester_authorized: true,
}
}
#[test]
fn a_document_change_surfaces_the_hidden_key_rotation() {
let effects = plan().to_effects();
let kinds: Vec<&str> = effects.iter().map(|e| e.kind.as_str()).collect();
assert_eq!(
kinds,
["documentChange", "keyRotation", "preRotationRefresh"]
);
let rotation = &effects[1];
assert_eq!(rotation.before, Some(json!(["z6MkOld"])));
assert_eq!(rotation.after, Some(json!(["z6MkNew"])));
assert!(
rotation.summary.contains("stops being able to authorize"),
"the summary must say what the rotation costs the holder: {}",
rotation.summary
);
}
#[test]
fn added_member_reports_no_before() {
let effects = plan().to_effects();
let doc = &effects[0];
assert_eq!(doc.path.as_deref(), Some("/service"));
assert!(doc.before.is_none(), "an added member has no prior value");
assert!(doc.summary.starts_with("Adds `service`"));
}
#[test]
fn no_rotation_when_keys_are_unchanged() {
let mut p = plan();
p.new_update_keys = p.prior_update_keys.clone();
p.pre_rotation_count = 0;
let kinds: Vec<String> = p.to_effects().iter().map(|e| e.kind.clone()).collect();
assert_eq!(kinds, ["documentChange"]);
}
#[test]
fn removal_reports_no_after() {
let mut p = plan();
std::mem::swap(&mut p.prior_document, &mut p.new_document);
let doc = &p.to_effects()[0];
assert!(
doc.after.is_none(),
"a removed member has no resulting value"
);
assert!(doc.summary.starts_with("Removes `service`"));
}
}