lib

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

verified_profile_conformance.rs (11093B)


      1 #![cfg(feature = "json")]
      2 
      3 use std::{borrow::Cow, fs, path::Path};
      4 
      5 use radroots_event::envelope::event_head::{
      6     CurrentEventHead, EventHeadCandidate, EventHeadCandidateResult, EventHeadDecision,
      7     event_head_candidate_for_event, select_event_head,
      8 };
      9 use radroots_event::{envelope::EventEnvelope, envelope::EventEnvelopeParts, wire::Nip01EventWire};
     10 use radroots_event::{id::ParseError, wire::EventWireError};
     11 use radroots_event_codec::admission::profile::{
     12     RadrootsAdmittedProfileEvent, RadrootsProfileAdmissionError, verify_and_admit_profile_event,
     13 };
     14 use radroots_event_codec::decode::profile::RadrootsProfileMetadataParseError;
     15 use radroots_event_codec::verify::{RadrootsNip01VerificationError, verify_nip01_event};
     16 use serde::Deserialize;
     17 use serde_json::{Map, Value};
     18 
     19 const PACKAGED_VECTORS: &str = include_str!("fixtures/profile_verified_event.v1.json");
     20 const WORKSPACE_VECTOR_PATH: &str =
     21     "../../contracts/conformance/vectors/profile/verified_event.v1.json";
     22 const WORKSPACE_CONTRACT_MARKER_PATH: &str = "../../contracts/manifest.toml";
     23 
     24 #[derive(Debug, Deserialize)]
     25 struct Suite {
     26     suite: String,
     27     contract_version: String,
     28     vectors: Vec<Vector>,
     29 }
     30 
     31 #[derive(Debug, Deserialize)]
     32 struct Vector {
     33     id: String,
     34     kind: String,
     35     input: Value,
     36     expected: Value,
     37 }
     38 
     39 #[derive(Debug, Deserialize)]
     40 #[serde(deny_unknown_fields)]
     41 struct RawEvent {
     42     id: String,
     43     pubkey: String,
     44     created_at: u64,
     45     kind: u32,
     46     tags: Vec<Vec<String>>,
     47     content: String,
     48     sig: String,
     49 }
     50 
     51 #[test]
     52 fn raw_signed_vectors_execute_against_verified_event_and_profile_boundaries() {
     53     let vectors = conformance_vectors();
     54     let suite: Suite = serde_json::from_str(&vectors).expect("verified Profile vectors must parse");
     55     assert_eq!(suite.suite, "verified_profile_event");
     56     assert_eq!(suite.contract_version, "1.0.0");
     57     assert!(!suite.vectors.is_empty());
     58 
     59     for vector in &suite.vectors {
     60         execute(vector);
     61     }
     62 }
     63 
     64 fn conformance_vectors() -> Cow<'static, str> {
     65     let workspace_path = Path::new(env!("CARGO_MANIFEST_DIR")).join(WORKSPACE_VECTOR_PATH);
     66     match fs::read_to_string(&workspace_path) {
     67         Ok(canonical) => {
     68             assert_eq!(
     69                 canonical,
     70                 PACKAGED_VECTORS,
     71                 "packaged verified Profile vectors must match {}",
     72                 workspace_path.display()
     73             );
     74             Cow::Owned(canonical)
     75         }
     76         Err(error)
     77             if error.kind() == std::io::ErrorKind::NotFound
     78                 && !Path::new(env!("CARGO_MANIFEST_DIR"))
     79                     .join(WORKSPACE_CONTRACT_MARKER_PATH)
     80                     .is_file() =>
     81         {
     82             Cow::Borrowed(PACKAGED_VECTORS)
     83         }
     84         Err(error) => panic!("failed to read {}: {error}", workspace_path.display()),
     85     }
     86 }
     87 
     88 fn execute(vector: &Vector) {
     89     match vector.kind.as_str() {
     90         "event.verify_nip01.valid" => verify_valid(vector),
     91         "event.verify_nip01.invalid_id" => verify_invalid_id(vector),
     92         "event.verify_nip01.invalid_signature" => verify_invalid_signature(vector),
     93         "event.wire.invalid_public_key" => reject_invalid_wire_public_key(vector),
     94         "event.verify_nip01.kind_overflow" => verify_kind_overflow(vector),
     95         "profile.verify_and_admit.valid" => profile_admit_valid(vector),
     96         "profile.verify_and_admit.invalid_kind" => profile_admit_invalid_kind(vector),
     97         "profile.verify_and_admit.invalid_metadata" => profile_admit_invalid_metadata(vector),
     98         "profile.select_equal_time_head" => profile_select_equal_time_head(vector),
     99         kind => panic!("{} uses unsupported vector kind {kind}", vector.id),
    100     }
    101 }
    102 
    103 fn verify_valid(vector: &Vector) {
    104     let verified = verify_nip01_event(canonical_envelope(input_str(vector, "event_json")))
    105         .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id));
    106     assert_eq!(
    107         verified.event().id_hex(),
    108         expected_str(vector, "event_id"),
    109         "{}",
    110         vector.id
    111     );
    112     assert_eq!(
    113         u64::from(verified.event().kind_u32()),
    114         vector.expected["kind"].as_u64().expect("expected.kind"),
    115         "{}",
    116         vector.id
    117     );
    118 }
    119 
    120 fn verify_invalid_id(vector: &Vector) {
    121     let error = verify_nip01_event(unchecked_id_envelope(input_str(vector, "event_json")))
    122         .expect_err("mismatched event id must fail");
    123     assert_eq!(error.code(), expected_str(vector, "error"));
    124     assert!(matches!(
    125         error,
    126         RadrootsNip01VerificationError::IdMismatch { .. }
    127     ));
    128 }
    129 
    130 fn verify_invalid_signature(vector: &Vector) {
    131     let error = verify_nip01_event(canonical_envelope(input_str(vector, "event_json")))
    132         .expect_err("invalid event signature must fail");
    133     assert_eq!(error.code(), expected_str(vector, "error"));
    134     assert_eq!(error, RadrootsNip01VerificationError::SignatureInvalid);
    135 }
    136 
    137 fn reject_invalid_wire_public_key(vector: &Vector) {
    138     let error = Nip01EventWire::parse_json(input_str(vector, "event_json"))
    139         .expect_err("invalid secp256k1 public key must fail wire parsing");
    140     assert_eq!(expected_str(vector, "error"), "invalid_public_key");
    141     assert_eq!(
    142         error,
    143         EventWireError::InvalidIdentifier {
    144             field: "pubkey",
    145             error: ParseError::InvalidPublicKey,
    146         }
    147     );
    148 }
    149 
    150 fn verify_kind_overflow(vector: &Vector) {
    151     let error = verify_nip01_event(canonical_envelope(input_str(vector, "event_json")))
    152         .expect_err("out-of-range event kind must fail");
    153     assert_eq!(error.code(), expected_str(vector, "error"));
    154     assert!(matches!(
    155         error,
    156         RadrootsNip01VerificationError::KindOutOfRange { kind }
    157             if u64::from(kind) == vector.expected["kind"].as_u64().expect("expected.kind")
    158     ));
    159 }
    160 
    161 fn profile_admit_valid(vector: &Vector) {
    162     let admitted = admitted_event(input_str(vector, "event_json"), &vector.id);
    163     assert_eq!(admitted.event().id_hex(), expected_str(vector, "event_id"));
    164     assert_eq!(
    165         admitted.verified_event().event().id_hex(),
    166         expected_str(vector, "event_id")
    167     );
    168     assert_eq!(
    169         serde_json::to_value(admitted.event().tags_as_vec()).expect("tags"),
    170         vector.expected["tags"]
    171     );
    172     assert_eq!(projected_metadata(&admitted), vector.expected["projected"]);
    173     assert_eq!(
    174         serde_json::to_value(admitted.metadata().residual_fields()).expect("residual fields"),
    175         vector.expected["residual_fields"]
    176     );
    177     let (verified, metadata) = admitted.into_parts();
    178     assert_eq!(verified.event().id_hex(), expected_str(vector, "event_id"));
    179     assert!(metadata.name().is_none());
    180 }
    181 
    182 fn profile_admit_invalid_kind(vector: &Vector) {
    183     let error = verify_and_admit_profile_event(canonical_envelope(input_str(vector, "event_json")))
    184         .expect_err("verified non-Profile kind must fail admission");
    185     assert_eq!(error.code(), expected_str(vector, "error"));
    186     assert!(matches!(
    187         error,
    188         RadrootsProfileAdmissionError::InvalidKind { expected: 0, actual }
    189             if u64::from(actual) == vector.expected["actual"].as_u64().expect("expected.actual")
    190     ));
    191 }
    192 
    193 fn profile_admit_invalid_metadata(vector: &Vector) {
    194     let error = verify_and_admit_profile_event(canonical_envelope(input_str(vector, "event_json")))
    195         .expect_err("verified Profile with non-object content must fail admission");
    196     assert_eq!(error.code(), expected_str(vector, "error"));
    197     assert_eq!(
    198         error,
    199         RadrootsProfileAdmissionError::Metadata(RadrootsProfileMetadataParseError::RootNotObject)
    200     );
    201 }
    202 
    203 fn profile_select_equal_time_head(vector: &Vector) {
    204     let first = admitted_event(input_str(vector, "first_event_json"), &vector.id);
    205     let second = admitted_event(input_str(vector, "second_event_json"), &vector.id);
    206     assert_eq!(
    207         first.event().created_at_u64(),
    208         second.event().created_at_u64()
    209     );
    210     assert_eq!(
    211         selected_event_id(&first, &second),
    212         expected_str(vector, "event_id")
    213     );
    214     assert_eq!(
    215         selected_event_id(&second, &first),
    216         expected_str(vector, "event_id")
    217     );
    218     assert_eq!(
    219         first.event().created_at_u64(),
    220         vector.expected["created_at"]
    221             .as_u64()
    222             .expect("expected.created_at")
    223     );
    224 }
    225 
    226 fn selected_event_id(
    227     current: &RadrootsAdmittedProfileEvent,
    228     candidate: &RadrootsAdmittedProfileEvent,
    229 ) -> String {
    230     let current: CurrentEventHead = head_candidate(current).into();
    231     match select_event_head(head_candidate(candidate), Some(&current)) {
    232         EventHeadDecision::Applied(head) => head.event_id.into_string(),
    233         EventHeadDecision::SkippedDuplicate
    234         | EventHeadDecision::SkippedOlder
    235         | EventHeadDecision::SkippedSameTimestampHigherEventId => current.event_id.into_string(),
    236         EventHeadDecision::CoordinateMismatch => {
    237             panic!("admitted Profile events from one author must share a coordinate")
    238         }
    239     }
    240 }
    241 
    242 fn head_candidate(admitted: &RadrootsAdmittedProfileEvent) -> EventHeadCandidate {
    243     match event_head_candidate_for_event(admitted.event()).expect("Profile contract") {
    244         EventHeadCandidateResult::Candidate(candidate) => candidate,
    245         other => panic!("admitted Profile must be a replaceable head candidate: {other:?}"),
    246     }
    247 }
    248 
    249 fn admitted_event(raw_json: &str, vector_id: &str) -> RadrootsAdmittedProfileEvent {
    250     verify_and_admit_profile_event(canonical_envelope(raw_json))
    251         .unwrap_or_else(|error| panic!("{vector_id} failed: {error}"))
    252 }
    253 
    254 fn canonical_envelope(raw_json: &str) -> EventEnvelope {
    255     Nip01EventWire::parse_json(raw_json)
    256         .expect("canonical raw event")
    257         .into_envelope()
    258         .expect("event envelope")
    259 }
    260 
    261 fn unchecked_id_envelope(raw_json: &str) -> EventEnvelope {
    262     let raw: RawEvent = serde_json::from_str(raw_json).expect("raw event");
    263     EventEnvelope::new(EventEnvelopeParts {
    264         id: raw.id,
    265         author: raw.pubkey,
    266         created_at: raw.created_at,
    267         kind: raw.kind,
    268         tags: raw.tags,
    269         content: raw.content,
    270         sig: raw.sig,
    271     })
    272     .expect("unchecked-id envelope")
    273 }
    274 
    275 fn projected_metadata(admitted: &RadrootsAdmittedProfileEvent) -> Value {
    276     let metadata = admitted.metadata();
    277     let mut projected = Map::new();
    278     if let Some(value) = metadata.name() {
    279         projected.insert("name".to_string(), Value::String(value.to_string()));
    280     }
    281     if let Some(value) = metadata.display_name() {
    282         projected.insert("display_name".to_string(), Value::String(value.to_string()));
    283     }
    284     if let Some(value) = metadata.bot() {
    285         projected.insert("bot".to_string(), Value::Bool(value));
    286     }
    287     Value::Object(projected)
    288 }
    289 
    290 fn input_str<'a>(vector: &'a Vector, field: &str) -> &'a str {
    291     vector.input[field]
    292         .as_str()
    293         .unwrap_or_else(|| panic!("{} input.{field} must be a string", vector.id))
    294 }
    295 
    296 fn expected_str<'a>(vector: &'a Vector, field: &str) -> &'a str {
    297     vector.expected[field]
    298         .as_str()
    299         .unwrap_or_else(|| panic!("{} expected.{field} must be a string", vector.id))
    300 }