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)
}
}
fn prefix_is_self_addressing(i: &str) -> bool {
i.is_empty() || i == SAID_PLACEHOLDER || i.starts_with('E')
}
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 inception_prefix = obj.get("i").and_then(|v| v.as_str()).unwrap_or("");
let blank_prefix = protocol.blanks_inception_prefix()
&& matches!(event_type, "icp" | "dip" | "vcp")
&& prefix_is_self_addressing(inception_prefix);
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 self_addressing_inception_said_independent_of_i() {
let event_placeholder = serde_json::json!({
"v": "KERI10JSON000000_",
"t": "icp",
"d": "",
"i": SAID_PLACEHOLDER,
"s": "0",
"kt": "1",
"k": ["DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"],
"nt": "1",
"n": ["EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"],
"bt": "0",
"b": [],
"a": []
});
let event_digest = serde_json::json!({
"v": "KERI10JSON000000_",
"t": "icp",
"d": "",
"i": "EOoC9AuwxiwcyUDsa2yNAaZOVWqfiAt4o3R31_8K2Z1J",
"s": "0",
"kt": "1",
"k": ["DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"],
"nt": "1",
"n": ["EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"],
"bt": "0",
"b": [],
"a": []
});
let said_placeholder = compute_said(&event_placeholder).unwrap();
let said_digest = compute_said(&event_digest).unwrap();
assert_eq!(
said_placeholder, said_digest,
"self-addressing inception SAID must be independent of the digest 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 basic_prefix_inception_keeps_i_matches_keripy() {
let raw = r#"{"v":"KERI10JSON0000fd_","t":"icp","d":"EAAD4cS7l9pm_N8JM9UsVeAZhwCIaDkSU341hbhHJbSf","i":"DAABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4f","s":"0","kt":"1","k":["DAABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4f"],"nt":"0","n":[],"bt":"0","b":[],"c":[],"a":[]}"#;
let event: serde_json::Value = serde_json::from_str(raw).unwrap();
let said = compute_said(&event).unwrap();
assert_eq!(
said.as_str(),
"EAAD4cS7l9pm_N8JM9UsVeAZhwCIaDkSU341hbhHJbSf",
"basic-prefix icp SAID must match keripy (i kept, not blanked)"
);
assert!(verify_said(&event).is_ok());
}
#[test]
fn self_addressing_inception_blanks_i_matches_keripy() {
let raw = r#"{"v":"KERI10JSON0000fd_","t":"icp","d":"EOoC9AuwxiwcyUDsa2yNAaZOVWqfiAt4o3R31_8K2Z1J","i":"EOoC9AuwxiwcyUDsa2yNAaZOVWqfiAt4o3R31_8K2Z1J","s":"0","kt":"1","k":["DAABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4f"],"nt":"0","n":[],"bt":"0","b":[],"c":[],"a":[]}"#;
let event: serde_json::Value = serde_json::from_str(raw).unwrap();
let said = compute_said(&event).unwrap();
assert_eq!(
said.as_str(),
"EOoC9AuwxiwcyUDsa2yNAaZOVWqfiAt4o3R31_8K2Z1J",
"self-addressing icp SAID must still blank i and match keripy"
);
assert!(verify_said(&event).is_ok());
}
#[test]
fn placeholder_inception_prefix_is_self_addressing() {
let base = serde_json::json!({
"v": "KERI10JSON000000_",
"t": "icp",
"d": "",
"s": "0",
"kt": "1",
"k": ["DAABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4f"],
"nt": "0",
"n": [],
"bt": "0",
"b": [],
"c": [],
"a": []
});
let mut with_placeholder = base.clone();
with_placeholder["i"] = serde_json::Value::String(SAID_PLACEHOLDER.to_string());
let mut with_digest = base.clone();
with_digest["i"] =
serde_json::Value::String("EOoC9AuwxiwcyUDsa2yNAaZOVWqfiAt4o3R31_8K2Z1J".to_string());
assert_eq!(
compute_said(&with_placeholder).unwrap(),
compute_said(&with_digest).unwrap(),
"placeholder and E-coded i are both self-addressing — same SAID"
);
assert!(prefix_is_self_addressing(SAID_PLACEHOLDER));
assert!(prefix_is_self_addressing(
"EOoC9AuwxiwcyUDsa2yNAaZOVWqfiAt4o3R31_8K2Z1J"
));
assert!(!prefix_is_self_addressing(
"DAABAgMEBQYHCAkKCwwNDg8QERITFBUWFxgZGhscHR4f"
));
}
#[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)"
);
}
}