use crate::body::Signature;
use crate::receipt::{
is_canonical_ms_utc, ms_rfc3339, preimage_hash_of_object_with, verify_signature_over_hash_with,
ReceiptSigner,
};
use acdp_jcs::try_canonicalize_value;
use acdp_primitives::error::AcdpError;
use acdp_primitives::primitives::{ContentHash, CtxId, LineageId};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
pub const LOG_LEAF_VERSION: &str = "acdp-log-leaf/1";
pub const LOG_CHECKPOINT_VERSION: &str = "acdp-log/1";
pub fn decode_sha256_hex(prefixed: &str) -> Result<[u8; 32], AcdpError> {
let hex_part = prefixed.strip_prefix("sha256:").ok_or_else(|| {
AcdpError::InvalidLogProof(format!(
"hash '{prefixed}' is not of the form 'sha256:<hex>' (RFC-ACDP-0012 §2)"
))
})?;
if hex_part.len() != 64
|| !hex_part
.bytes()
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
{
return Err(AcdpError::InvalidLogProof(format!(
"hash '{prefixed}' is not 'sha256:' + 64 lowercase hex digits (RFC-ACDP-0012 §2)"
)));
}
let bytes = hex::decode(hex_part)
.map_err(|e| AcdpError::InvalidLogProof(format!("hash '{prefixed}': {e}")))?;
bytes.try_into().map_err(|_| {
AcdpError::InvalidLogProof(format!("hash '{prefixed}' does not encode 32 bytes"))
})
}
pub fn encode_sha256_hex(digest: &[u8; 32]) -> String {
format!("sha256:{}", hex::encode(digest))
}
pub fn parse_log_id(log_id: &str) -> Result<(&str, &str), AcdpError> {
let (did, instance) = log_id.split_once("/log/").ok_or_else(|| {
AcdpError::InvalidLogProof(format!(
"log_id '{log_id}' is not '<registry_did>/log/<instance>' (RFC-ACDP-0012 §6)"
))
})?;
let msi = did.strip_prefix("did:web:").ok_or_else(|| {
AcdpError::InvalidLogProof(format!(
"log_id '{log_id}' registry DID must be did:web (RFC-ACDP-0012 §6)"
))
})?;
if msi.is_empty()
|| !msi
.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'%' | b':' | b'-'))
{
return Err(AcdpError::InvalidLogProof(format!(
"log_id '{log_id}' has a malformed did:web method-specific identifier"
)));
}
if instance.is_empty()
|| instance.len() > 32
|| !instance
.bytes()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-')
{
return Err(AcdpError::InvalidLogProof(format!(
"log_id '{log_id}' instance component must match [a-z0-9-]{{1,32}} (RFC-ACDP-0012 §6)"
)));
}
Ok((did, instance))
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LogLeaf {
pub leaf_version: String,
pub ctx_id: CtxId,
pub lineage_id: LineageId,
pub origin_registry: String,
#[serde(with = "ms_rfc3339")]
pub created_at: DateTime<Utc>,
pub content_hash: ContentHash,
pub key_fingerprint: String,
pub receipt_hash: String,
}
impl LogLeaf {
pub fn from_value(value: &serde_json::Value) -> Result<Self, AcdpError> {
let leaf = Self::deserialize(value)
.map_err(|e| AcdpError::InvalidLogProof(format!("log leaf does not parse: {e}")))?;
if leaf.leaf_version != LOG_LEAF_VERSION {
return Err(AcdpError::InvalidLogProof(format!(
"log leaf leaf_version '{}' ≠ '{LOG_LEAF_VERSION}' (RFC-ACDP-0012 §4)",
leaf.leaf_version
)));
}
let raw = value
.get("created_at")
.and_then(|v| v.as_str())
.ok_or_else(|| {
AcdpError::InvalidLogProof("log leaf created_at missing or not a string".into())
})?;
if !is_canonical_ms_utc(raw) {
return Err(AcdpError::InvalidLogProof(format!(
"log leaf created_at '{raw}' is not canonical millisecond-precision RFC 3339 \
UTC (`YYYY-MM-DDTHH:MM:SS.mmmZ`, RFC-ACDP-0012 §4)"
)));
}
Ok(leaf)
}
pub fn canonical_bytes(&self) -> Result<Vec<u8>, AcdpError> {
try_canonicalize_value(&serde_json::to_value(self)?)
}
pub fn leaf_hash(&self) -> Result<[u8; 32], AcdpError> {
Ok(acdp_crypto::merkle::leaf_hash(&self.canonical_bytes()?))
}
pub fn leaf_hash_hex(&self) -> Result<String, AcdpError> {
Ok(encode_sha256_hex(&self.leaf_hash()?))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LogCheckpoint {
pub checkpoint_version: String,
pub log_id: String,
pub tree_size: u64,
pub root_hash: String,
#[serde(with = "ms_rfc3339")]
pub timestamp: DateTime<Utc>,
pub signature: Signature,
}
impl LogCheckpoint {
pub fn from_value(value: &serde_json::Value) -> Result<Self, AcdpError> {
let checkpoint = Self::deserialize(value).map_err(|e| {
AcdpError::InvalidLogProof(format!("log_checkpoint does not parse: {e}"))
})?;
if checkpoint.checkpoint_version != LOG_CHECKPOINT_VERSION {
return Err(AcdpError::InvalidLogProof(format!(
"log_checkpoint checkpoint_version '{}' ≠ '{LOG_CHECKPOINT_VERSION}' \
(RFC-ACDP-0012 §9.3 step 1)",
checkpoint.checkpoint_version
)));
}
parse_log_id(&checkpoint.log_id)?;
decode_sha256_hex(&checkpoint.root_hash)?;
let raw = value
.get("timestamp")
.and_then(|v| v.as_str())
.ok_or_else(|| {
AcdpError::InvalidLogProof(
"log_checkpoint timestamp missing or not a string".into(),
)
})?;
if !is_canonical_ms_utc(raw) {
return Err(AcdpError::InvalidLogProof(format!(
"log_checkpoint timestamp '{raw}' is not canonical millisecond-precision \
RFC 3339 UTC (`YYYY-MM-DDTHH:MM:SS.mmmZ`, RFC-ACDP-0012 §9.3 step 4)"
)));
}
checkpoint.registry_did()?;
Ok(checkpoint)
}
pub fn registry_did(&self) -> Result<&str, AcdpError> {
let (did, _instance) = parse_log_id(&self.log_id)?;
match self.signature.key_id.split_once('#') {
Some((key_did, frag)) if key_did == did && !frag.is_empty() => Ok(did),
_ => Err(AcdpError::InvalidLogProof(format!(
"log_checkpoint signature.key_id '{}' is not a DID URL under the log_id's \
registry DID '{did}' (RFC-ACDP-0012 §6)",
self.signature.key_id
))),
}
}
pub fn preimage_hash_of_value(value: &serde_json::Value) -> Result<ContentHash, AcdpError> {
preimage_hash_of_object_with(value, "log_checkpoint", AcdpError::InvalidLogProof)
}
pub fn preimage_hash(&self) -> Result<ContentHash, AcdpError> {
Self::preimage_hash_of_value(&serde_json::to_value(self)?)
}
pub fn root_hash_bytes(&self) -> Result<[u8; 32], AcdpError> {
decode_sha256_hex(&self.root_hash)
}
pub fn verify_signature_with_key(
&self,
registry_pub_ed25519: Option<&[u8; 32]>,
registry_pub_p256_sec1: Option<&[u8]>,
) -> Result<(), AcdpError> {
let hash = self.preimage_hash()?;
self.verify_signature_against_hash(&hash, registry_pub_ed25519, registry_pub_p256_sec1)
}
pub fn verify_signature_against_hash(
&self,
hash: &ContentHash,
registry_pub_ed25519: Option<&[u8; 32]>,
registry_pub_p256_sec1: Option<&[u8]>,
) -> Result<(), AcdpError> {
verify_signature_over_hash_with(
&self.signature,
hash,
registry_pub_ed25519,
registry_pub_p256_sec1,
"log_checkpoint",
AcdpError::InvalidLogProof,
)
}
pub fn cross_check_registry_binding(
&self,
serving_authority: &str,
capabilities_registry_did: &str,
) -> Result<(), AcdpError> {
let did = self.registry_did()?;
let expected_did = acdp_did::web::authority_to_did_web(serving_authority);
if did != expected_did {
return Err(AcdpError::InvalidLogProof(format!(
"log_checkpoint log_id registry DID '{did}' ≠ serving authority's DID \
'{expected_did}' (RFC-ACDP-0012 §9.3 step 3)"
)));
}
if did != capabilities_registry_did {
return Err(AcdpError::InvalidLogProof(format!(
"log_checkpoint log_id registry DID '{did}' ≠ capabilities.registry_did \
'{capabilities_registry_did}' (RFC-ACDP-0012 §9.3 step 3)"
)));
}
Ok(())
}
pub fn check_timestamp_skew(
&self,
now: DateTime<Utc>,
max_clock_skew: chrono::Duration,
) -> Result<(), AcdpError> {
if self.timestamp.timestamp_subsec_nanos() % 1_000_000 != 0 {
return Err(AcdpError::InvalidLogProof(
"log_checkpoint timestamp is not millisecond-truncated (RFC-ACDP-0001 §5.3)".into(),
));
}
if self.timestamp > now + max_clock_skew {
return Err(AcdpError::InvalidLogProof(format!(
"log_checkpoint timestamp '{}' is in the future beyond the {}s clock-skew \
allowance (consumer clock '{}') (RFC-ACDP-0012 §9.3 step 4)",
self.timestamp.format("%Y-%m-%dT%H:%M:%S%.3fZ"),
max_clock_skew.num_seconds(),
now.format("%Y-%m-%dT%H:%M:%S%.3fZ"),
)));
}
Ok(())
}
pub fn age_at(&self, now: DateTime<Utc>) -> chrono::Duration {
now - self.timestamp
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LogInclusion {
pub log_id: String,
pub leaf_index: u64,
pub tree_size: u64,
pub inclusion_path: Vec<String>,
pub log_checkpoint: LogCheckpoint,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub leaf: Option<LogLeaf>,
}
impl LogInclusion {
pub fn from_value(value: &serde_json::Value) -> Result<Self, AcdpError> {
let inclusion = Self::deserialize(value).map_err(|e| {
AcdpError::InvalidLogProof(format!("log_inclusion does not parse: {e}"))
})?;
if inclusion.tree_size < 1 {
return Err(AcdpError::InvalidLogProof(
"log_inclusion tree_size must be >= 1 (an empty tree has no members)".into(),
));
}
if let Some(cp) = value.get("log_checkpoint") {
LogCheckpoint::from_value(cp)?;
}
if let Some(leaf) = value.get("leaf") {
LogLeaf::from_value(leaf)?;
}
Ok(inclusion)
}
pub fn cross_check_binding(&self) -> Result<(), AcdpError> {
if self.tree_size != self.log_checkpoint.tree_size {
return Err(AcdpError::InvalidLogProof(format!(
"log_inclusion tree_size {} ≠ log_checkpoint.tree_size {} \
(RFC-ACDP-0012 §9.1 step 4)",
self.tree_size, self.log_checkpoint.tree_size
)));
}
if self.log_id != self.log_checkpoint.log_id {
return Err(AcdpError::InvalidLogProof(format!(
"log_inclusion log_id '{}' ≠ log_checkpoint.log_id '{}' \
(RFC-ACDP-0012 §9.1 step 4)",
self.log_id, self.log_checkpoint.log_id
)));
}
if self.leaf_index >= self.tree_size {
return Err(AcdpError::InvalidLogProof(format!(
"log_inclusion leaf_index {} is not < tree_size {} (RFC-ACDP-0012 §9.1 step 4)",
self.leaf_index, self.tree_size
)));
}
Ok(())
}
pub fn verify_leaf_hash(&self, leaf_hash: &[u8; 32]) -> Result<(), AcdpError> {
self.cross_check_binding()?;
let path = self
.inclusion_path
.iter()
.map(|p| decode_sha256_hex(p))
.collect::<Result<Vec<_>, _>>()?;
let root = self.log_checkpoint.root_hash_bytes()?;
if !acdp_crypto::merkle::verify_inclusion(
leaf_hash,
self.leaf_index,
self.tree_size,
&path,
&root,
) {
return Err(AcdpError::InvalidLogProof(format!(
"inclusion_path for leaf_index {} does not fold to the checkpoint root_hash \
'{}' at tree_size {} (RFC-ACDP-0012 §9.1 step 6)",
self.leaf_index, self.log_checkpoint.root_hash, self.tree_size
)));
}
Ok(())
}
pub fn verify_reconstructed_leaf(&self, leaf: &LogLeaf) -> Result<(), AcdpError> {
self.verify_leaf_hash(&leaf.leaf_hash()?)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct LogConsistencyProof {
pub log_id: String,
pub first_tree_size: u64,
pub second_tree_size: u64,
pub consistency_path: Vec<String>,
pub log_checkpoint: LogCheckpoint,
}
impl LogConsistencyProof {
pub fn from_value(value: &serde_json::Value) -> Result<Self, AcdpError> {
let proof = Self::deserialize(value).map_err(|e| {
AcdpError::InvalidLogProof(format!("consistency proof does not parse: {e}"))
})?;
if let Some(cp) = value.get("log_checkpoint") {
LogCheckpoint::from_value(cp)?;
}
Ok(proof)
}
pub fn verify_against_first_root(&self, first_root_hash: &str) -> Result<(), AcdpError> {
if self.log_id != self.log_checkpoint.log_id {
return Err(AcdpError::InvalidLogProof(format!(
"consistency proof log_id '{}' ≠ log_checkpoint.log_id '{}' \
(consistency proofs exist only within one log_id, RFC-ACDP-0012 §7.4)",
self.log_id, self.log_checkpoint.log_id
)));
}
if self.second_tree_size != self.log_checkpoint.tree_size {
return Err(AcdpError::InvalidLogProof(format!(
"consistency proof second_tree_size {} ≠ log_checkpoint.tree_size {} \
(RFC-ACDP-0012 §8.2)",
self.second_tree_size, self.log_checkpoint.tree_size
)));
}
let first_root = decode_sha256_hex(first_root_hash)?;
let second_root = self.log_checkpoint.root_hash_bytes()?;
let path = self
.consistency_path
.iter()
.map(|p| decode_sha256_hex(p))
.collect::<Result<Vec<_>, _>>()?;
if !acdp_crypto::merkle::verify_consistency(
self.first_tree_size,
self.second_tree_size,
&path,
&first_root,
&second_root,
) {
return Err(AcdpError::InvalidLogProof(format!(
"consistency_path does not prove the tree at size {} is a prefix of the tree \
at size {} — evidence of a logged-history rewrite; retain both checkpoints \
and this path (RFC-ACDP-0012 §9.2)",
self.first_tree_size, self.second_tree_size
)));
}
Ok(())
}
}
impl ReceiptSigner {
pub fn mint_log_checkpoint(
&self,
log_id: &str,
tree_size: u64,
root_hash: &str,
timestamp: DateTime<Utc>,
) -> Result<LogCheckpoint, AcdpError> {
let (did, _instance) = parse_log_id(log_id)?;
if did != self.registry_did() {
return Err(AcdpError::SchemaViolation(format!(
"cannot mint a checkpoint for log_id '{log_id}' under registry_did '{}' \
(RFC-ACDP-0012 §6: the log_id embeds the registry's own DID)",
self.registry_did()
)));
}
decode_sha256_hex(root_hash)?;
let mut checkpoint = LogCheckpoint {
checkpoint_version: LOG_CHECKPOINT_VERSION.to_string(),
log_id: log_id.to_string(),
tree_size,
root_hash: root_hash.to_string(),
timestamp: acdp_primitives::time::trunc_ms(timestamp),
signature: Signature {
algorithm: self.signing_key().algorithm().into(),
key_id: self.key_id().to_string(),
value: String::new(), },
};
let hash = checkpoint.preimage_hash()?;
let (algorithm, value) = self.signing_key().sign_content_hash(&hash);
checkpoint.signature.algorithm = algorithm.into();
checkpoint.signature.value = value;
Ok(checkpoint)
}
}
#[cfg(test)]
mod tests {
use super::*;
use acdp_crypto::SigningKey;
const REGISTRY_DID: &str = "did:web:registry.example.com";
const LOG_ID: &str = "did:web:registry.example.com/log/1";
fn signer() -> ReceiptSigner {
ReceiptSigner::new(
SigningKey::from_bytes(&[0x11u8; 32]),
REGISTRY_DID,
format!("{REGISTRY_DID}#receipt-key-1"),
)
.unwrap()
}
fn registry_pub() -> [u8; 32] {
SigningKey::from_bytes(&[0x11u8; 32]).verifying_key_bytes()
}
fn leaf(i: u8) -> LogLeaf {
let ctx_id =
format!("acdp://registry.example.com/00000000-0000-4000-8000-0000000000{i:02}");
LogLeaf {
leaf_version: LOG_LEAF_VERSION.into(),
lineage_id: acdp_crypto::derive_lineage_id(&CtxId(ctx_id.clone())),
ctx_id: CtxId(ctx_id),
origin_registry: "registry.example.com".into(),
created_at: chrono::DateTime::parse_from_rfc3339("2026-07-01T01:00:00.123Z")
.unwrap()
.with_timezone(&Utc),
content_hash: ContentHash(format!("sha256:{}", "b".repeat(64))),
key_fingerprint: format!("sha256:{}", "c".repeat(64)),
receipt_hash: format!("sha256:{}", "d".repeat(64)),
}
}
#[test]
fn leaf_closed_schema_and_version_enforced() {
let l = leaf(1);
let wire = serde_json::to_value(&l).unwrap();
assert_eq!(LogLeaf::from_value(&wire).unwrap(), l);
let mut extra = wire.clone();
extra
.as_object_mut()
.unwrap()
.insert("note".into(), serde_json::json!("x"));
assert!(matches!(
LogLeaf::from_value(&extra).unwrap_err(),
AcdpError::InvalidLogProof(_)
));
let mut wrong = wire.clone();
wrong["leaf_version"] = serde_json::json!("acdp-log-leaf/2");
assert!(LogLeaf::from_value(&wrong).is_err());
let mut bad_ts = wire.clone();
bad_ts["created_at"] = serde_json::json!("2026-07-01T01:00:00Z");
assert!(LogLeaf::from_value(&bad_ts).is_err());
}
#[test]
fn checkpoint_mint_verify_round_trip() {
let root = encode_sha256_hex(&acdp_crypto::merkle_tree_hash(&[]));
let cp = signer()
.mint_log_checkpoint(LOG_ID, 0, &root, Utc::now())
.unwrap();
cp.verify_signature_with_key(Some(®istry_pub()), None)
.expect("freshly minted checkpoint must verify");
let wire = serde_json::to_value(&cp).unwrap();
let parsed = LogCheckpoint::from_value(&wire).unwrap();
let raw_hash = LogCheckpoint::preimage_hash_of_value(&wire).unwrap();
assert_eq!(raw_hash, cp.preimage_hash().unwrap());
parsed
.verify_signature_against_hash(&raw_hash, Some(®istry_pub()), None)
.unwrap();
parsed
.cross_check_registry_binding("registry.example.com", REGISTRY_DID)
.unwrap();
assert!(parsed
.cross_check_registry_binding("hostile.example", "did:web:hostile.example")
.is_err());
parsed
.check_timestamp_skew(Utc::now(), chrono::Duration::seconds(120))
.unwrap();
let mut tampered = wire.clone();
tampered["root_hash"] = serde_json::json!(format!("sha256:{}", "f".repeat(64)));
let t = LogCheckpoint::from_value(&tampered).unwrap();
let t_hash = LogCheckpoint::preimage_hash_of_value(&tampered).unwrap();
let err = t
.verify_signature_against_hash(&t_hash, Some(®istry_pub()), None)
.unwrap_err();
assert!(matches!(err, AcdpError::InvalidLogProof(_)), "got {err:?}");
}
#[test]
fn checkpoint_closed_schema_and_domain_separation() {
let root = encode_sha256_hex(&acdp_crypto::merkle_tree_hash(&[]));
let cp = signer()
.mint_log_checkpoint(LOG_ID, 0, &root, Utc::now())
.unwrap();
let mut wire = serde_json::to_value(&cp).unwrap();
wire.as_object_mut()
.unwrap()
.insert("witnesses".into(), serde_json::json!([]));
assert!(matches!(
LogCheckpoint::from_value(&wire).unwrap_err(),
AcdpError::InvalidLogProof(_)
));
let mut wire = serde_json::to_value(&cp).unwrap();
wire["checkpoint_version"] = serde_json::json!("acdp-lhr/1");
assert!(LogCheckpoint::from_value(&wire).is_err());
let wire = serde_json::to_value(&cp).unwrap();
assert!(crate::receipt::LineageHeadReceipt::from_value(&wire).is_err());
assert!(signer()
.mint_log_checkpoint("did:web:evil.example.com/log/1", 0, &root, Utc::now())
.is_err());
assert!(signer()
.mint_log_checkpoint(
"did:web:registry.example.com/log/UPPER",
0,
&root,
Utc::now()
)
.is_err());
assert!(signer()
.mint_log_checkpoint(LOG_ID, 0, "sha256:short", Utc::now())
.is_err());
}
#[test]
fn inclusion_and_consistency_round_trip() {
let leaves: Vec<LogLeaf> = (0..5).map(leaf).collect();
let hashes: Vec<[u8; 32]> = leaves.iter().map(|l| l.leaf_hash().unwrap()).collect();
let root = acdp_crypto::merkle_tree_hash(&hashes);
let cp = signer()
.mint_log_checkpoint(LOG_ID, 5, &encode_sha256_hex(&root), Utc::now())
.unwrap();
for (i, l) in leaves.iter().enumerate() {
let path = acdp_crypto::inclusion_path(i, &hashes).unwrap();
let inclusion = LogInclusion {
log_id: LOG_ID.into(),
leaf_index: i as u64,
tree_size: 5,
inclusion_path: path.iter().map(encode_sha256_hex).collect(),
log_checkpoint: cp.clone(),
leaf: None,
};
inclusion.verify_reconstructed_leaf(l).unwrap();
let wire = serde_json::to_value(&inclusion).unwrap();
assert!(wire.get("leaf").is_none(), "absent, never null");
LogInclusion::from_value(&wire)
.unwrap()
.verify_leaf_hash(&hashes[i])
.unwrap();
if !inclusion.inclusion_path.is_empty() {
let mut bad = inclusion.clone();
let mut raw = decode_sha256_hex(&bad.inclusion_path[0]).unwrap();
raw[0] ^= 1;
bad.inclusion_path[0] = encode_sha256_hex(&raw);
let err = bad.verify_reconstructed_leaf(l).unwrap_err();
assert!(matches!(err, AcdpError::InvalidLogProof(_)), "got {err:?}");
}
}
let path = acdp_crypto::inclusion_path(0, &hashes).unwrap();
let mk = |leaf_index, tree_size, log_id: &str| LogInclusion {
log_id: log_id.into(),
leaf_index,
tree_size,
inclusion_path: path.iter().map(encode_sha256_hex).collect(),
log_checkpoint: cp.clone(),
leaf: None,
};
assert!(mk(0, 4, LOG_ID).verify_leaf_hash(&hashes[0]).is_err()); assert!(mk(5, 5, LOG_ID).verify_leaf_hash(&hashes[0]).is_err()); assert!(mk(0, 5, "did:web:registry.example.com/log/2")
.verify_leaf_hash(&hashes[0])
.is_err());
let first_root = acdp_crypto::merkle_tree_hash(&hashes[..3]);
let proof = LogConsistencyProof {
log_id: LOG_ID.into(),
first_tree_size: 3,
second_tree_size: 5,
consistency_path: acdp_crypto::consistency_proof(3, &hashes)
.unwrap()
.iter()
.map(encode_sha256_hex)
.collect(),
log_checkpoint: cp.clone(),
};
proof
.verify_against_first_root(&encode_sha256_hex(&first_root))
.unwrap();
let err = proof
.verify_against_first_root(&format!("sha256:{}", "e".repeat(64)))
.unwrap_err();
assert!(matches!(err, AcdpError::InvalidLogProof(_)), "got {err:?}");
}
#[test]
fn log_id_parsing() {
assert_eq!(
parse_log_id(LOG_ID).unwrap(),
("did:web:registry.example.com", "1")
);
for bad in [
"did:web:registry.example.com", "did:key:z6Mk/log/1", "did:web:registry.example.com/log/", "did:web:registry.example.com/log/UP", &format!("did:web:r.example/log/{}", "a".repeat(33)), ] {
assert!(parse_log_id(bad).is_err(), "{bad} must be rejected");
}
}
}