lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

admission_authority.rs (16156B)


      1 use super::{
      2     ConformanceVectorEntry, OperationsContractManifest, collect_non_empty_set,
      3     validate_conformance_vector_file, validate_operation_case_kinds,
      4 };
      5 use serde_json::{Map, Value};
      6 use std::{
      7     collections::{BTreeMap, BTreeSet},
      8     fs,
      9     path::Path,
     10 };
     11 
     12 pub(super) const ADMISSION_CONFORMANCE_VECTOR_RELATIVE: &str =
     13     "contracts/conformance/vectors/event/verified_admission.v1.json";
     14 
     15 const REQUIRED_ADMISSION_PUBLIC_TYPES: [&str; 4] = [
     16     "RadrootsSignatureVerifiedEvent",
     17     "RadrootsContractValidatedEvent",
     18     "RadrootsAdmittedEvent",
     19     "RadrootsEventAdmissionError",
     20 ];
     21 
     22 const ADMISSION_CASE_KINDS: [&str; 2] =
     23     ["event.admit_verified.valid", "event.admit_verified.invalid"];
     24 
     25 #[derive(Clone, Copy)]
     26 enum ExpectedAdmission {
     27     Valid {
     28         variant: &'static str,
     29         contract_id: &'static str,
     30     },
     31     Invalid {
     32         error_variant: &'static str,
     33         error_code: &'static str,
     34     },
     35 }
     36 
     37 #[derive(Clone, Copy)]
     38 struct AdmissionVectorExpectation {
     39     id: &'static str,
     40     outcome: ExpectedAdmission,
     41 }
     42 
     43 const ADMISSION_VECTOR_EXPECTATIONS: [AdmissionVectorExpectation; 13] = [
     44     AdmissionVectorExpectation {
     45         id: "event_admit_verified_profile_001",
     46         outcome: ExpectedAdmission::Valid {
     47             variant: "profile",
     48             contract_id: "radroots.profile.metadata.v1",
     49         },
     50     },
     51     AdmissionVectorExpectation {
     52         id: "event_admit_verified_root_update_002",
     53         outcome: ExpectedAdmission::Valid {
     54             variant: "root_post",
     55             contract_id: "radroots.social.update.v1",
     56         },
     57     },
     58     AdmissionVectorExpectation {
     59         id: "event_admit_verified_root_photo_update_003",
     60         outcome: ExpectedAdmission::Valid {
     61             variant: "root_post",
     62             contract_id: "radroots.social.photo_update.v1",
     63         },
     64     },
     65     AdmissionVectorExpectation {
     66         id: "event_admit_verified_root_ask_004",
     67         outcome: ExpectedAdmission::Valid {
     68             variant: "root_post",
     69             contract_id: "radroots.social.ask.v1",
     70         },
     71     },
     72     AdmissionVectorExpectation {
     73         id: "event_admit_verified_post_to_reply_005",
     74         outcome: ExpectedAdmission::Valid {
     75             variant: "reply",
     76             contract_id: "radroots.social.reply.v1",
     77         },
     78     },
     79     AdmissionVectorExpectation {
     80         id: "event_admit_verified_comment_006",
     81         outcome: ExpectedAdmission::Valid {
     82             variant: "comment",
     83             contract_id: "radroots.social.comment.v1",
     84         },
     85     },
     86     AdmissionVectorExpectation {
     87         id: "event_admit_verified_deletion_request_007",
     88         outcome: ExpectedAdmission::Valid {
     89             variant: "deletion_request",
     90             contract_id: "radroots.social.deletion_request.v1",
     91         },
     92     },
     93     AdmissionVectorExpectation {
     94         id: "event_admit_verified_food_availability_008",
     95         outcome: ExpectedAdmission::Valid {
     96             variant: "food_availability",
     97             contract_id: "radroots.food.availability.v1",
     98         },
     99     },
    100     AdmissionVectorExpectation {
    101         id: "event_admit_verified_operational_fallback_009",
    102         outcome: ExpectedAdmission::Valid {
    103             variant: "contract_validated",
    104             contract_id: "radroots.operational_listing.published.v1",
    105         },
    106     },
    107     AdmissionVectorExpectation {
    108         id: "event_admit_verified_unsupported_kind_010",
    109         outcome: ExpectedAdmission::Invalid {
    110             error_variant: "contract_match",
    111             error_code: "unsupported_kind",
    112         },
    113     },
    114     AdmissionVectorExpectation {
    115         id: "event_admit_verified_generic_nip99_excluded_011",
    116         outcome: ExpectedAdmission::Invalid {
    117             error_variant: "contract_match",
    118             error_code: "unsupported_shape",
    119         },
    120     },
    121     AdmissionVectorExpectation {
    122         id: "event_admit_verified_operational_invalid_shape_012",
    123         outcome: ExpectedAdmission::Invalid {
    124             error_variant: "contract_validation",
    125             error_code: "missing_tag",
    126         },
    127     },
    128     AdmissionVectorExpectation {
    129         id: "event_admit_verified_ambiguous_food_markers_013",
    130         outcome: ExpectedAdmission::Invalid {
    131             error_variant: "food_availability",
    132             error_code: "food_profile_ambiguous",
    133         },
    134     },
    135 ];
    136 
    137 pub(super) fn validate_admission_operation_authority(
    138     manifest: &OperationsContractManifest,
    139     workspace_root: &Path,
    140 ) -> Result<(), String> {
    141     let vector = validate_conformance_vector_file(
    142         &workspace_root.join(ADMISSION_CONFORMANCE_VECTOR_RELATIVE),
    143         &manifest.contract.version,
    144     )?;
    145     validate_admission_operation_inventory(manifest, &vector)?;
    146     validate_source_witnesses(workspace_root)?;
    147     Ok(())
    148 }
    149 
    150 pub(super) fn validate_admission_operation_inventory(
    151     manifest: &OperationsContractManifest,
    152     vector: &super::ConformanceVectorFile,
    153 ) -> Result<(), String> {
    154     validate_manifest_authority(manifest, vector)?;
    155     validate_vector_inventory(&vector.vectors)
    156 }
    157 
    158 fn validate_manifest_authority(
    159     manifest: &OperationsContractManifest,
    160     vector: &super::ConformanceVectorFile,
    161 ) -> Result<(), String> {
    162     let shared_types = collect_non_empty_set(
    163         &manifest.shared_types.public,
    164         "verified admission shared_types.public",
    165     )?;
    166     for required in REQUIRED_ADMISSION_PUBLIC_TYPES {
    167         if !shared_types.contains(required) {
    168             return Err(format!(
    169                 "verified admission authority requires shared public type {required}"
    170             ));
    171         }
    172     }
    173 
    174     let actual_keys = manifest
    175         .operations
    176         .iter()
    177         .filter(|(key, operation)| {
    178             key.starts_with("event_admit_verified")
    179                 || operation.id.starts_with("event.admit_verified")
    180                 || operation.conformance.vector == ADMISSION_CONFORMANCE_VECTOR_RELATIVE
    181         })
    182         .map(|(key, _)| key.as_str())
    183         .collect::<BTreeSet<_>>();
    184     let expected_keys = BTreeSet::from(["event_admit_verified"]);
    185     if actual_keys != expected_keys {
    186         return Err(format!(
    187             "verified admission operation authority drift: expected {expected_keys:?}, got {actual_keys:?}"
    188         ));
    189     }
    190 
    191     let operation = manifest
    192         .operations
    193         .get("event_admit_verified")
    194         .ok_or_else(|| {
    195             "verified admission operation event_admit_verified is required".to_string()
    196         })?;
    197     require_scalar("domain", &operation.domain, "event")?;
    198     require_scalar("id", &operation.id, "event.admit_verified")?;
    199     require_scalar("stability", &operation.stability, "beta")?;
    200     require_scalar("error_class", &operation.error_class, "admission_error")?;
    201     require_scalar("signing", &operation.signing, "none")?;
    202     require_scalar("transport", &operation.transport, "none")?;
    203     if !operation.deterministic {
    204         return Err(
    205             "verified admission operation deterministic drift: expected true, got false"
    206                 .to_string(),
    207         );
    208     }
    209     require_sequence(
    210         "inputs",
    211         &operation.inputs,
    212         &["RadrootsSignatureVerifiedEvent"],
    213     )?;
    214     require_sequence("outputs", &operation.outputs, &["RadrootsAdmittedEvent"])?;
    215     require_sequence(
    216         "implementation.rust_modules",
    217         &operation.implementation.rust_modules,
    218         &[
    219             "crates/event_codec/src/admission.rs",
    220             "crates/event_codec/src/verification.rs",
    221         ],
    222     )?;
    223     require_sequence(
    224         "implementation.rust_types",
    225         &operation.implementation.rust_types,
    226         &[
    227             "radroots_event_codec::admission::RadrootsAdmittedEvent",
    228             "radroots_event_codec::admission::RadrootsEventAdmissionError",
    229             "radroots_event_codec::verify::RadrootsContractValidatedEvent",
    230             "radroots_event_codec::verify::RadrootsSignatureVerifiedEvent",
    231         ],
    232     )?;
    233     require_scalar(
    234         "conformance.vector",
    235         &operation.conformance.vector,
    236         ADMISSION_CONFORMANCE_VECTOR_RELATIVE,
    237     )?;
    238     validate_operation_case_kinds(operation, vector)?;
    239     require_sequence(
    240         "conformance.case_kinds",
    241         &operation.conformance.case_kinds,
    242         &ADMISSION_CASE_KINDS,
    243     )
    244 }
    245 
    246 fn validate_source_witnesses(workspace_root: &Path) -> Result<(), String> {
    247     for (relative, fragments) in [
    248         (
    249             "crates/event_codec/src/admission.rs",
    250             &[
    251                 "pub enum RadrootsAdmittedEvent",
    252                 "pub enum RadrootsEventAdmissionError",
    253                 "pub fn admit_verified_event(",
    254             ][..],
    255         ),
    256         (
    257             "crates/event_codec/src/verification/v1.rs",
    258             &[
    259                 "pub struct RadrootsSignatureVerifiedEvent",
    260                 "pub struct RadrootsContractValidatedEvent",
    261             ][..],
    262         ),
    263     ] {
    264         let source = fs::read_to_string(workspace_root.join(relative)).map_err(|error| {
    265             format!("failed to read verified admission witness {relative}: {error}")
    266         })?;
    267         for fragment in fragments {
    268             if !source.contains(fragment) {
    269                 return Err(format!(
    270                     "verified admission witness {relative} is missing `{fragment}`"
    271                 ));
    272             }
    273         }
    274     }
    275     Ok(())
    276 }
    277 
    278 fn validate_vector_inventory(vectors: &[ConformanceVectorEntry]) -> Result<(), String> {
    279     let expected = ADMISSION_VECTOR_EXPECTATIONS
    280         .iter()
    281         .map(|entry| (entry.id, entry.outcome))
    282         .collect::<BTreeMap<_, _>>();
    283     let mut actual = BTreeMap::new();
    284     let mut event_ids = BTreeSet::new();
    285 
    286     for vector in vectors {
    287         let expectation = expected.get(vector.id.as_str()).ok_or_else(|| {
    288             format!(
    289                 "verified admission conformance vector has unexpected id {}",
    290                 vector.id
    291             )
    292         })?;
    293         if actual
    294             .insert(vector.id.as_str(), vector.kind.as_str())
    295             .is_some()
    296         {
    297             return Err(format!(
    298                 "verified admission conformance vector has duplicate id {}",
    299                 vector.id
    300             ));
    301         }
    302         validate_vector(vector, *expectation, &mut event_ids)?;
    303     }
    304 
    305     let expected_inventory = ADMISSION_VECTOR_EXPECTATIONS
    306         .iter()
    307         .map(|entry| {
    308             let kind = match entry.outcome {
    309                 ExpectedAdmission::Valid { .. } => "event.admit_verified.valid",
    310                 ExpectedAdmission::Invalid { .. } => "event.admit_verified.invalid",
    311             };
    312             (entry.id, kind)
    313         })
    314         .collect::<BTreeMap<_, _>>();
    315     if actual != expected_inventory {
    316         return Err(format!(
    317             "verified admission conformance inventory drift: expected {expected_inventory:?}, got {actual:?}"
    318         ));
    319     }
    320     Ok(())
    321 }
    322 
    323 fn validate_vector(
    324     vector: &ConformanceVectorEntry,
    325     expectation: ExpectedAdmission,
    326     event_ids: &mut BTreeSet<String>,
    327 ) -> Result<(), String> {
    328     let input = exact_object(&vector.input, &["event"], &format!("{}.input", vector.id))?;
    329     let event = exact_object(
    330         &input["event"],
    331         &[
    332             "content",
    333             "created_at",
    334             "id",
    335             "kind",
    336             "pubkey",
    337             "sig",
    338             "tags",
    339         ],
    340         &format!("{}.input.event", vector.id),
    341     )?;
    342     let event_id = required_string(event, "id", &vector.id)?;
    343     let pubkey = required_string(event, "pubkey", &vector.id)?;
    344     let signature = required_string(event, "sig", &vector.id)?;
    345     require_lower_hex(event_id, 64, "event id", &vector.id)?;
    346     require_lower_hex(pubkey, 64, "pubkey", &vector.id)?;
    347     require_lower_hex(signature, 128, "signature", &vector.id)?;
    348     if !event_ids.insert(event_id.to_string()) {
    349         return Err(format!(
    350             "verified admission vector {} reuses event id {event_id}",
    351             vector.id
    352         ));
    353     }
    354     if !event["created_at"].is_u64()
    355         || !event["kind"].is_u64()
    356         || !event["tags"].is_array()
    357         || !event["content"].is_string()
    358     {
    359         return Err(format!(
    360             "verified admission vector {} must contain a complete typed signed event",
    361             vector.id
    362         ));
    363     }
    364 
    365     match expectation {
    366         ExpectedAdmission::Valid {
    367             variant,
    368             contract_id,
    369         } => {
    370             if vector.kind != "event.admit_verified.valid" {
    371                 return Err(format!(
    372                     "verified admission vector {} kind drift",
    373                     vector.id
    374                 ));
    375             }
    376             let expected = exact_object(
    377                 vector.expected_value()?,
    378                 &["contract_id", "event_id", "variant"],
    379                 &format!("{}.expected", vector.id),
    380             )?;
    381             require_expected(expected, "variant", variant, &vector.id)?;
    382             require_expected(expected, "contract_id", contract_id, &vector.id)?;
    383             require_expected(expected, "event_id", event_id, &vector.id)?;
    384         }
    385         ExpectedAdmission::Invalid {
    386             error_variant,
    387             error_code,
    388         } => {
    389             if vector.kind != "event.admit_verified.invalid" {
    390                 return Err(format!(
    391                     "verified admission vector {} kind drift",
    392                     vector.id
    393                 ));
    394             }
    395             let expected = exact_object(
    396                 vector.expected_value()?,
    397                 &["error_code", "error_variant", "event_id"],
    398                 &format!("{}.expected", vector.id),
    399             )?;
    400             require_expected(expected, "error_variant", error_variant, &vector.id)?;
    401             require_expected(expected, "error_code", error_code, &vector.id)?;
    402             require_expected(expected, "event_id", event_id, &vector.id)?;
    403         }
    404     }
    405     Ok(())
    406 }
    407 
    408 fn exact_object<'a>(
    409     value: &'a Value,
    410     fields: &[&str],
    411     label: &str,
    412 ) -> Result<&'a Map<String, Value>, String> {
    413     let object = value
    414         .as_object()
    415         .ok_or_else(|| format!("verified admission {label} must be an object"))?;
    416     let actual = object.keys().map(String::as_str).collect::<BTreeSet<_>>();
    417     let expected = fields.iter().copied().collect::<BTreeSet<_>>();
    418     if actual != expected {
    419         return Err(format!(
    420             "verified admission {label} field inventory drift: expected {expected:?}, got {actual:?}"
    421         ));
    422     }
    423     Ok(object)
    424 }
    425 
    426 fn required_string<'a>(
    427     object: &'a Map<String, Value>,
    428     field: &str,
    429     vector_id: &str,
    430 ) -> Result<&'a str, String> {
    431     object[field].as_str().ok_or_else(|| {
    432         format!("verified admission vector {vector_id} field {field} must be a string")
    433     })
    434 }
    435 
    436 fn require_lower_hex(
    437     value: &str,
    438     length: usize,
    439     label: &str,
    440     vector_id: &str,
    441 ) -> Result<(), String> {
    442     if value.len() != length
    443         || !value.bytes().all(|byte| byte.is_ascii_hexdigit())
    444         || value.bytes().any(|byte| byte.is_ascii_uppercase())
    445     {
    446         return Err(format!(
    447             "verified admission vector {vector_id} {label} must be {length} lowercase hex characters"
    448         ));
    449     }
    450     Ok(())
    451 }
    452 
    453 fn require_expected(
    454     object: &Map<String, Value>,
    455     field: &str,
    456     expected: &str,
    457     vector_id: &str,
    458 ) -> Result<(), String> {
    459     let actual = required_string(object, field, vector_id)?;
    460     if actual != expected {
    461         return Err(format!(
    462             "verified admission vector {vector_id} expected.{field} drift: expected {expected}, got {actual}"
    463         ));
    464     }
    465     Ok(())
    466 }
    467 
    468 fn require_scalar(field: &str, actual: &str, expected: &str) -> Result<(), String> {
    469     if actual != expected {
    470         return Err(format!(
    471             "verified admission operation {field} drift: expected {expected}, got {actual}"
    472         ));
    473     }
    474     Ok(())
    475 }
    476 
    477 fn require_sequence(field: &str, actual: &[String], expected: &[&str]) -> Result<(), String> {
    478     if !actual
    479         .iter()
    480         .map(String::as_str)
    481         .eq(expected.iter().copied())
    482     {
    483         return Err(format!(
    484             "verified admission operation {field} drift: expected {expected:?}, got {actual:?}"
    485         ));
    486     }
    487     Ok(())
    488 }