use crate::error::KeriTranslationError;
use crate::types::Said;
pub const SAID_PLACEHOLDER: &str = "############################################";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Protocol {
Keri,
Acdc,
}
impl Protocol {
fn version_placeholder(self) -> &'static str {
match self {
Protocol::Keri => "KERI10JSON000000_",
Protocol::Acdc => "ACDC10JSON000000_",
}
}
fn code(self) -> &'static str {
match self {
Protocol::Keri => "KERI",
Protocol::Acdc => "ACDC",
}
}
fn blanks_inception_prefix(self) -> bool {
matches!(self, Protocol::Keri)
}
}
pub fn compute_said(event: &serde_json::Value) -> Result<Said, KeriTranslationError> {
compute_said_with_protocol(event, Protocol::Keri)
}
pub fn compute_said_with_protocol(
event: &serde_json::Value,
protocol: Protocol,
) -> Result<Said, KeriTranslationError> {
let obj = event
.as_object()
.ok_or(KeriTranslationError::MissingField {
field: "root object",
})?;
let placeholder = serde_json::Value::String(SAID_PLACEHOLDER.to_string());
let event_type = obj.get("t").and_then(|v| v.as_str()).unwrap_or("");
let blank_prefix =
protocol.blanks_inception_prefix() && matches!(event_type, "icp" | "dip" | "vcp");
let mut new_obj = serde_json::Map::new();
for (k, v) in obj {
if k == "x" {
continue;
} else if k == "d" {
new_obj.insert("d".to_string(), placeholder.clone());
} else if k == "i" && blank_prefix {
new_obj.insert("i".to_string(), placeholder.clone());
} else {
new_obj.insert(k.clone(), v.clone());
}
}
if !new_obj.contains_key("d") {
new_obj.insert("d".to_string(), placeholder.clone());
}
let version_placeholder = protocol.version_placeholder();
new_obj.insert(
"v".to_string(),
serde_json::Value::String(version_placeholder.to_string()),
);
let pass1 = serde_json::to_vec(&serde_json::Value::Object(new_obj.clone()))
.map_err(KeriTranslationError::SerializationFailed)?;
let version_string = format!("{}10JSON{:06x}_", protocol.code(), pass1.len());
debug_assert_eq!(version_string.len(), version_placeholder.len());
new_obj.insert("v".to_string(), serde_json::Value::String(version_string));
let serialized = serde_json::to_vec(&serde_json::Value::Object(new_obj))
.map_err(KeriTranslationError::SerializationFailed)?;
let hash = blake3::hash(&serialized);
#[allow(clippy::expect_used)] let said = crate::cesr_encode::encode_blake3_digest(hash.as_bytes())
.expect("32-byte Blake3 digest always encodes as a CESR Blake3_256 SAID");
Ok(Said::new_unchecked(said))
}
pub fn compute_section_said(section: &serde_json::Value) -> Result<Said, KeriTranslationError> {
let obj = section
.as_object()
.ok_or(KeriTranslationError::MissingField {
field: "section object",
})?;
let placeholder = serde_json::Value::String(SAID_PLACEHOLDER.to_string());
let mut new_obj = serde_json::Map::new();
for (k, v) in obj {
if k == "d" {
new_obj.insert("d".to_string(), placeholder.clone());
} else {
new_obj.insert(k.clone(), v.clone());
}
}
if !new_obj.contains_key("d") {
new_obj.insert("d".to_string(), placeholder.clone());
}
let serialized = serde_json::to_vec(&serde_json::Value::Object(new_obj))
.map_err(KeriTranslationError::SerializationFailed)?;
let hash = blake3::hash(&serialized);
#[allow(clippy::expect_used)] let said = crate::cesr_encode::encode_blake3_digest(hash.as_bytes())
.expect("32-byte Blake3 digest always encodes as a CESR Blake3_256 SAID");
Ok(Said::new_unchecked(said))
}
pub fn verify_said(event: &serde_json::Value) -> Result<(), KeriTranslationError> {
let found = event
.get("d")
.and_then(|v| v.as_str())
.ok_or(KeriTranslationError::MissingField { field: "d" })?
.to_string();
let computed = compute_said(event)?;
if computed.as_str() != found {
return Err(KeriTranslationError::SaidMismatch {
computed: computed.into_inner(),
found,
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn said_has_correct_length() {
let event = serde_json::json!({
"v": "KERI10JSON000000_",
"t": "icp",
"d": "",
"i": "",
"s": "0",
"kt": "1",
"k": ["DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"],
"nt": "1",
"n": ["EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"],
"bt": "0",
"b": [],
"a": []
});
let said = compute_said(&event).unwrap();
assert_eq!(said.as_str().len(), 44);
assert!(said.as_str().starts_with('E'));
}
#[test]
fn said_is_deterministic() {
let event = serde_json::json!({
"v": "KERI10JSON000000_",
"t": "rot",
"d": "",
"i": "EExistingPrefix",
"s": "1",
"p": "EPreviousSaid",
"kt": "1",
"k": ["DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"],
"nt": "1",
"n": ["EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"],
"bt": "0",
"b": [],
"a": []
});
let said1 = compute_said(&event).unwrap();
let said2 = compute_said(&event).unwrap();
assert_eq!(said1, said2);
}
#[test]
fn said_ignores_x_field() {
let event_with_x = serde_json::json!({
"v": "KERI10JSON000000_",
"t": "icp",
"d": "",
"i": "",
"s": "0",
"kt": "1",
"k": ["DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"],
"nt": "1",
"n": ["EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"],
"bt": "0",
"b": [],
"a": [],
"x": "abcdef1234567890"
});
let event_without_x = serde_json::json!({
"v": "KERI10JSON000000_",
"t": "icp",
"d": "",
"i": "",
"s": "0",
"kt": "1",
"k": ["DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"],
"nt": "1",
"n": ["EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"],
"bt": "0",
"b": [],
"a": []
});
let said_with = compute_said(&event_with_x).unwrap();
let said_without = compute_said(&event_without_x).unwrap();
assert_eq!(said_with, said_without, "x field must not affect SAID");
}
#[test]
fn inception_applies_i_placeholder() {
let event_a = serde_json::json!({
"v": "KERI10JSON000000_",
"t": "icp",
"d": "",
"i": "some_prefix_a",
"s": "0",
"kt": "1",
"k": ["DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"],
"nt": "1",
"n": ["EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"],
"bt": "0",
"b": [],
"a": []
});
let event_b = serde_json::json!({
"v": "KERI10JSON000000_",
"t": "icp",
"d": "",
"i": "some_prefix_b",
"s": "0",
"kt": "1",
"k": ["DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"],
"nt": "1",
"n": ["EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"],
"bt": "0",
"b": [],
"a": []
});
let said_a = compute_said(&event_a).unwrap();
let said_b = compute_said(&event_b).unwrap();
assert_eq!(
said_a, said_b,
"inception SAID must be independent of initial i value"
);
}
#[test]
fn verify_said_accepts_correct() {
let event = serde_json::json!({
"v": "KERI10JSON000000_",
"t": "rot",
"d": "",
"i": "EExistingPrefix",
"s": "1",
"p": "EPreviousSaid",
"kt": "1",
"k": ["DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"],
"nt": "1",
"n": ["EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"],
"bt": "0",
"b": [],
"a": []
});
let said = compute_said(&event).unwrap();
let mut event_with_said = event.clone();
event_with_said["d"] = serde_json::Value::String(said.into_inner());
assert!(verify_said(&event_with_said).is_ok());
}
#[test]
fn verify_said_rejects_wrong() {
let event = serde_json::json!({
"v": "KERI10JSON000000_",
"t": "rot",
"d": "Ewrong_said_value_that_doesnt_match_at_all!",
"i": "EExistingPrefix",
"s": "1",
"p": "EPreviousSaid",
"kt": "1",
"k": ["DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"],
"nt": "1",
"n": ["EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"],
"bt": "0",
"b": [],
"a": []
});
assert!(verify_said(&event).is_err());
}
#[test]
fn serde_json_map_preserves_insertion_order() {
let json = r#"{"z":"last","a":"first","m":"middle"}"#;
let parsed: serde_json::Value = serde_json::from_str(json).unwrap();
let keys: Vec<&str> = parsed
.as_object()
.unwrap()
.keys()
.map(|k| k.as_str())
.collect();
assert_eq!(
keys,
vec!["z", "a", "m"],
"serde_json::Map must preserve insertion order (preserve_order feature required)"
);
}
}