use oas_crypto::proof::AgentLineageProof;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LineageSection {
pub human_root_did: String,
pub creator_did: String,
pub generation: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub derivation_proof: Option<AgentLineageProof>,
pub human_root_chain: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub org_inclusion_proof: Option<OrgInclusionProof>,
#[serde(skip_serializing_if = "Option::is_none")]
pub anchor_refs: Option<Vec<AnchorRef>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AnchorRef {
pub scheme: String,
pub locator: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub inclusion_proof: Option<AnchorInclusionProof>,
}
impl AnchorRef {
pub fn to_uri(&self) -> String {
format!("anchor:{}:{}", self.scheme, self.locator)
}
pub fn parse_uri(uri: &str) -> Option<Self> {
let rest = uri.strip_prefix("anchor:")?;
let (scheme, locator) = rest.split_once(':')?;
if scheme.is_empty() || locator.is_empty() {
return None;
}
Some(Self {
scheme: scheme.to_string(),
locator: locator.to_string(),
inclusion_proof: None,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AnchorInclusionProof {
pub proof_type: String,
pub root: String,
pub leaf_hash: String,
#[serde(default)]
pub path: Vec<MerkleProofNode>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum MerkleDirection {
Left,
Right,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MerkleProofNode {
pub sibling_hash: String,
pub direction: MerkleDirection,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OrgInclusionProof {
pub merkle_root: String,
pub leaf_hash: String,
pub path: Vec<MerkleProofNode>,
}
#[cfg(test)]
mod tests {
use super::*;
fn section_with_refs(refs: Option<Vec<AnchorRef>>) -> LineageSection {
LineageSection {
human_root_did: "did:oas:ns:hmr:alice".to_string(),
creator_did: "did:oas:ns:hmr:alice".to_string(),
generation: 0,
derivation_proof: None,
human_root_chain: vec!["did:oas:ns:hmr:alice".to_string()],
org_inclusion_proof: None,
anchor_refs: refs,
}
}
#[test]
fn anchor_refs_round_trip_camel_case() {
let refs = vec![
AnchorRef {
scheme: "sigil".to_string(),
locator: "tx:0xabc123".to_string(),
inclusion_proof: Some(AnchorInclusionProof {
proof_type: "merkle-blake3".to_string(),
root: "blake3:aa".to_string(),
leaf_hash: "blake3:bb".to_string(),
path: vec![MerkleProofNode {
sibling_hash: "blake3:cc".to_string(),
direction: MerkleDirection::Left,
}],
}),
},
AnchorRef {
scheme: "eas".to_string(),
locator: "1:0xdef456".to_string(),
inclusion_proof: None,
},
];
let lineage = section_with_refs(Some(refs.clone()));
let json = serde_json::to_string(&lineage).expect("serialize");
assert!(json.contains("\"anchorRefs\""));
assert!(json.contains("\"inclusionProof\""));
assert!(json.contains("\"proofType\""));
assert!(json.contains("\"leafHash\""));
let parsed: LineageSection = serde_json::from_str(&json).expect("deserialize");
assert_eq!(parsed.anchor_refs.as_deref(), Some(refs.as_slice()));
}
#[test]
fn absent_anchor_refs_are_omitted_from_json() {
let lineage = section_with_refs(None);
let json = serde_json::to_string(&lineage).expect("serialize");
assert!(!json.contains("anchorRefs"));
}
#[test]
fn unknown_schemes_are_tolerated_and_ignored_not_rejected() {
let json = r##"{
"humanRootDid": "did:oas:ns:hmr:alice",
"creatorDid": "did:oas:ns:hmr:alice",
"generation": 0,
"humanRootChain": ["did:oas:ns:hmr:alice"],
"anchorRefs": [
{"scheme": "future-scheme-2031", "locator": "whatever"},
{"scheme": "sigil", "locator": "tx:0xabc"}
]
}"##;
let parsed: LineageSection =
serde_json::from_str(json).expect("deserializes with unknown schemes");
let refs = parsed.anchor_refs.as_deref().expect("refs present");
let supported: Vec<&AnchorRef> = refs
.iter()
.filter(|r| matches!(r.scheme.as_str(), "sigil" | "eas" | "ctlog"))
.collect();
assert_eq!(supported.len(), 1);
assert_eq!(supported[0].scheme, "sigil");
}
#[test]
fn anchor_ref_uri_display_and_parse() {
let reference = AnchorRef {
scheme: "eas".to_string(),
locator: "1:0xdef456".to_string(),
inclusion_proof: None,
};
let uri = reference.to_uri();
assert_eq!(uri, "anchor:eas:1:0xdef456");
let parsed = AnchorRef::parse_uri(&uri).expect("parses");
assert_eq!(parsed.scheme, "eas");
assert_eq!(parsed.locator, "1:0xdef456");
assert!(AnchorRef::parse_uri("not-an-anchor-ref").is_none());
assert!(AnchorRef::parse_uri("anchor:eas:").is_none());
}
#[test]
fn serde_round_trip() {
let lineage = LineageSection {
human_root_did: "did:oas:ns:hmr:alice".to_string(),
creator_did: "did:oas:ns:hmr:alice".to_string(),
generation: 0,
derivation_proof: None,
human_root_chain: vec![
"did:oas:ns:ao:org".to_string(),
"did:oas:ns:hmr:alice".to_string(),
],
org_inclusion_proof: None,
anchor_refs: None,
};
let json = serde_json::to_string(&lineage).unwrap_or_default();
let parsed: LineageSection =
serde_json::from_str(&json).unwrap_or_else(|_| lineage.clone());
assert_eq!(lineage, parsed);
}
#[test]
fn generation_matches_chain_minus_one() {
let lineage = LineageSection {
human_root_did: "did:oas:ns:hmr:alice".to_string(),
creator_did: "did:oas:ns:ao:org".to_string(),
generation: 2,
derivation_proof: None,
human_root_chain: vec![
"did:oas:ns:agent:bot".to_string(),
"did:oas:ns:ao:org".to_string(),
"did:oas:ns:hmr:alice".to_string(),
],
org_inclusion_proof: None,
anchor_refs: None,
};
assert_eq!(
lineage.generation as usize,
lineage.human_root_chain.len() - 1
);
}
#[test]
fn org_inclusion_proof_round_trips_through_serde() {
let proof = OrgInclusionProof {
merkle_root: "blake3:rootabc".into(),
leaf_hash: "blake3:leafdef".into(),
path: vec![
MerkleProofNode {
sibling_hash: "blake3:sibling1".into(),
direction: MerkleDirection::Left,
},
MerkleProofNode {
sibling_hash: "blake3:sibling2".into(),
direction: MerkleDirection::Right,
},
],
};
let json = serde_json::to_string(&proof).expect("serialize");
assert!(json.contains("\"direction\":\"left\""));
assert!(json.contains("\"direction\":\"right\""));
let back: OrgInclusionProof = serde_json::from_str(&json).expect("deserialize");
assert_eq!(proof, back);
}
#[test]
fn org_inclusion_proof_field_is_optional_in_lineage() {
let lineage = LineageSection {
human_root_did: "did:oas:ns:mhr:org".into(),
creator_did: "did:oas:ns:mhr:org".into(),
generation: 1,
derivation_proof: None,
human_root_chain: vec!["did:oas:ns:mhr:org".into()],
org_inclusion_proof: None,
anchor_refs: None,
};
let json = serde_json::to_string(&lineage).expect("ser");
assert!(!json.contains("orgInclusionProof"));
}
}