lib

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

wire.rs (19501B)


      1 //! Bounded, proof-carrying storage wire for authored event plans.
      2 
      3 #[cfg(all(not(feature = "std"), feature = "json"))]
      4 use alloc::string::ToString;
      5 #[cfg(not(feature = "std"))]
      6 use alloc::{borrow::ToOwned, string::String, vec::Vec};
      7 #[cfg(feature = "std")]
      8 use std::{borrow::ToOwned, string::String, vec::Vec};
      9 
     10 use core::fmt;
     11 #[cfg(feature = "json")]
     12 use radroots_event::{
     13     contract::{ContractId, EventAuthoringPolicy, validate_event_contract_parts},
     14     envelope::EventTags,
     15     wire::compute_canonical_nip01_event_id,
     16 };
     17 use radroots_event::{
     18     contract::{ContractIdentityError, ContractKey, RegistryVersion},
     19     id::EventId,
     20     wire::v1::DEFAULT_RAW_JSON_MAX_BYTES,
     21 };
     22 use radroots_identity::PublicKey;
     23 
     24 #[cfg(feature = "json")]
     25 use super::{AuthoredEventBody, typed::validate_historical_typed_profile};
     26 use super::{AuthoredEventPlan, PLAN_WIRE_VERSION_V1, PlanDigest, PlanDigestError};
     27 
     28 /// Hard byte limit applied before JSON parsing or field allocation.
     29 pub const PLAN_WIRE_MAX_BYTES: usize = DEFAULT_RAW_JSON_MAX_BYTES;
     30 
     31 /// Exact version-one durable representation of an authored event plan.
     32 ///
     33 /// Construction is limited to validated plans. Deserialization is intentionally
     34 /// implemented only through [`Self::from_json`], which validates the embedded
     35 /// historical registry profile and both cryptographic commitments.
     36 #[derive(Clone, Debug, PartialEq, Eq)]
     37 pub struct PlanWireV1 {
     38     schema_version: u32,
     39     contract: ContractKey,
     40     expected_author: PublicKey,
     41     created_at: u64,
     42     kind: u32,
     43     tags: Vec<Vec<String>>,
     44     content: String,
     45     expected_event_id: EventId,
     46     plan_digest: PlanDigest,
     47 }
     48 
     49 impl PlanWireV1 {
     50     #[must_use]
     51     pub fn from_plan(plan: &AuthoredEventPlan) -> Self {
     52         Self {
     53             schema_version: PLAN_WIRE_VERSION_V1,
     54             contract: plan.body().contract().clone(),
     55             expected_author: *plan.author(),
     56             created_at: plan.created_at(),
     57             kind: plan.body().kind(),
     58             tags: plan.body().tags().to_vec(),
     59             content: plan.body().content().to_owned(),
     60             expected_event_id: *plan.expected_event_id(),
     61             plan_digest: plan.digest(),
     62         }
     63     }
     64 
     65     #[must_use]
     66     pub const fn schema_version(&self) -> u32 {
     67         self.schema_version
     68     }
     69 
     70     #[must_use]
     71     pub const fn contract(&self) -> &ContractKey {
     72         &self.contract
     73     }
     74 
     75     #[must_use]
     76     pub const fn expected_author(&self) -> &PublicKey {
     77         &self.expected_author
     78     }
     79 
     80     #[must_use]
     81     pub const fn created_at(&self) -> u64 {
     82         self.created_at
     83     }
     84 
     85     #[must_use]
     86     pub const fn kind(&self) -> u32 {
     87         self.kind
     88     }
     89 
     90     #[must_use]
     91     pub fn tags(&self) -> &[Vec<String>] {
     92         &self.tags
     93     }
     94 
     95     #[must_use]
     96     pub fn content(&self) -> &str {
     97         &self.content
     98     }
     99 
    100     #[must_use]
    101     pub const fn expected_event_id(&self) -> &EventId {
    102         &self.expected_event_id
    103     }
    104 
    105     #[must_use]
    106     pub const fn plan_digest(&self) -> PlanDigest {
    107         self.plan_digest
    108     }
    109 
    110     #[cfg(feature = "json")]
    111     pub fn to_json(&self) -> Result<Vec<u8>, PlanDecodeError> {
    112         serde_json::to_vec(self).map_err(|error| PlanDecodeError::Json(error.to_string()))
    113     }
    114 
    115     #[cfg(feature = "json")]
    116     pub fn from_json(bytes: &[u8]) -> Result<HistoricalPlanIntegrity, PlanDecodeError> {
    117         if bytes.len() > PLAN_WIRE_MAX_BYTES {
    118             return Err(PlanDecodeError::RawJsonTooLarge {
    119                 max: PLAN_WIRE_MAX_BYTES,
    120                 actual: bytes.len(),
    121             });
    122         }
    123         let wire = serde_json::from_slice::<RawPlanWireV1>(bytes)
    124             .map_err(|error| PlanDecodeError::Json(error.to_string()))?;
    125         Self::validate_raw(wire)
    126     }
    127 
    128     #[cfg(feature = "json")]
    129     fn validate_raw(wire: RawPlanWireV1) -> Result<HistoricalPlanIntegrity, PlanDecodeError> {
    130         if wire.schema_version != PLAN_WIRE_VERSION_V1 {
    131             return Err(PlanDecodeError::UnsupportedSchemaVersion {
    132                 actual: wire.schema_version,
    133             });
    134         }
    135 
    136         let registry_version = RegistryVersion::new(wire.contract_registry_version)
    137             .map_err(PlanDecodeError::ContractIdentity)?;
    138         let contract_id =
    139             ContractId::parse(wire.contract_id).map_err(PlanDecodeError::ContractIdentity)?;
    140         let contract = ContractKey::new(registry_version, contract_id)
    141             .map_err(PlanDecodeError::ContractIdentity)?;
    142         let definition = contract.contract();
    143         if definition.kind != wire.kind {
    144             return Err(PlanDecodeError::ContractKindMismatch {
    145                 expected: definition.kind,
    146                 actual: wire.kind,
    147             });
    148         }
    149         match definition.authoring_policy() {
    150             EventAuthoringPolicy::GenericDraft => {
    151                 validate_event_contract_parts(wire.kind, &wire.tags, &wire.content, definition.id)
    152                     .map_err(|error| PlanDecodeError::HistoricalShape(error.code().to_owned()))?;
    153             }
    154             EventAuthoringPolicy::TypedOnly => validate_historical_typed_profile(
    155                 definition.id,
    156                 wire.created_at,
    157                 &wire.expected_author,
    158                 wire.kind,
    159                 &wire.tags,
    160                 &wire.content,
    161             )
    162             .map_err(PlanDecodeError::HistoricalShape)?,
    163             EventAuthoringPolicy::ReadOnly => {
    164                 return Err(PlanDecodeError::HistoricalProfileUnavailable {
    165                     contract_id: definition.id.to_owned(),
    166                 });
    167             }
    168         }
    169         EventTags::new(wire.tags.clone())
    170             .map_err(|error| PlanDecodeError::HistoricalShape(error.to_string()))?;
    171         let expected_author = PublicKey::from_hex(&wire.expected_author)
    172             .map_err(|error| PlanDecodeError::ExpectedAuthor(error.to_string()))?;
    173         if expected_author.to_hex() != wire.expected_author {
    174             return Err(PlanDecodeError::NonCanonicalExpectedAuthor);
    175         }
    176         let expected_event_id = EventId::parse(&wire.expected_event_id)
    177             .map_err(|error| PlanDecodeError::ExpectedEventId(error.to_string()))?;
    178         if expected_event_id.to_hex() != wire.expected_event_id {
    179             return Err(PlanDecodeError::NonCanonicalExpectedEventId);
    180         }
    181         let recomputed_event_id = compute_canonical_nip01_event_id(
    182             &wire.expected_author,
    183             wire.created_at,
    184             wire.kind,
    185             &wire.tags,
    186             &wire.content,
    187         )
    188         .map_err(|error| PlanDecodeError::ExpectedEventId(error.to_string()))?;
    189         if recomputed_event_id != expected_event_id {
    190             return Err(PlanDecodeError::EventIdMismatch {
    191                 declared: expected_event_id.to_hex(),
    192                 computed: recomputed_event_id.to_hex(),
    193             });
    194         }
    195 
    196         let declared_digest =
    197             PlanDigest::parse_hex(&wire.plan_digest).map_err(PlanDecodeError::PlanDigest)?;
    198         let body = AuthoredEventBody {
    199             contract: contract.clone(),
    200             kind: wire.kind,
    201             tags: wire.tags,
    202             content: wire.content,
    203         };
    204         let plan = AuthoredEventPlan::from_validated_parts(
    205             body,
    206             expected_author,
    207             wire.created_at,
    208             expected_event_id,
    209         );
    210         if plan.digest() != declared_digest {
    211             return Err(PlanDecodeError::PlanDigestMismatch {
    212                 declared: declared_digest.to_hex(),
    213                 computed: plan.digest().to_hex(),
    214             });
    215         }
    216 
    217         Ok(HistoricalPlanIntegrity { plan })
    218     }
    219 }
    220 
    221 /// Proof that a plan reconstructed successfully under its embedded registry.
    222 #[derive(Clone, Debug, PartialEq, Eq)]
    223 pub struct HistoricalPlanIntegrity {
    224     plan: AuthoredEventPlan,
    225 }
    226 
    227 impl HistoricalPlanIntegrity {
    228     #[must_use]
    229     pub const fn plan(&self) -> &AuthoredEventPlan {
    230         &self.plan
    231     }
    232 
    233     #[must_use]
    234     pub fn into_plan(self) -> AuthoredEventPlan {
    235         self.plan
    236     }
    237 
    238     /// Compares registry versions for policy reporting only.
    239     ///
    240     /// This relation is not current signing authorization. Signing must obtain
    241     /// an explicit authorization decision from `radroots_signing`.
    242     #[must_use]
    243     pub fn registry_relation(&self, current: RegistryVersion) -> PlanRegistryRelation {
    244         if self.plan.body().contract().registry_version() == current {
    245             PlanRegistryRelation::Current
    246         } else {
    247             PlanRegistryRelation::Historical
    248         }
    249     }
    250 }
    251 
    252 /// Informational relation between an intact historical plan and current policy.
    253 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
    254 pub enum PlanRegistryRelation {
    255     Current,
    256     Historical,
    257 }
    258 
    259 #[derive(Clone, Debug, PartialEq, Eq)]
    260 pub enum PlanDecodeError {
    261     RawJsonTooLarge { max: usize, actual: usize },
    262     Json(String),
    263     UnsupportedSchemaVersion { actual: u32 },
    264     ContractIdentity(ContractIdentityError),
    265     ContractKindMismatch { expected: u32, actual: u32 },
    266     HistoricalProfileUnavailable { contract_id: String },
    267     HistoricalShape(String),
    268     ExpectedAuthor(String),
    269     NonCanonicalExpectedAuthor,
    270     ExpectedEventId(String),
    271     NonCanonicalExpectedEventId,
    272     EventIdMismatch { declared: String, computed: String },
    273     PlanDigest(PlanDigestError),
    274     PlanDigestMismatch { declared: String, computed: String },
    275 }
    276 
    277 impl fmt::Display for PlanDecodeError {
    278     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    279         match self {
    280             Self::RawJsonTooLarge { max, actual } => {
    281                 write!(formatter, "plan wire is {actual} bytes; max is {max}")
    282             }
    283             Self::Json(error) => write!(formatter, "plan wire JSON is invalid: {error}"),
    284             Self::UnsupportedSchemaVersion { actual } => {
    285                 write!(formatter, "unsupported plan wire schema version {actual}")
    286             }
    287             Self::ContractIdentity(error) => write!(formatter, "invalid contract key: {error}"),
    288             Self::ContractKindMismatch { expected, actual } => write!(
    289                 formatter,
    290                 "plan kind {actual} does not match historical contract kind {expected}"
    291             ),
    292             Self::HistoricalProfileUnavailable { contract_id } => write!(
    293                 formatter,
    294                 "historical authoring profile unavailable for `{contract_id}`"
    295             ),
    296             Self::HistoricalShape(error) => {
    297                 write!(formatter, "historical plan shape is invalid: {error}")
    298             }
    299             Self::ExpectedAuthor(error) => write!(formatter, "invalid expected author: {error}"),
    300             Self::NonCanonicalExpectedAuthor => {
    301                 formatter.write_str("expected author is not canonical lowercase hexadecimal")
    302             }
    303             Self::ExpectedEventId(error) => write!(formatter, "invalid expected event ID: {error}"),
    304             Self::NonCanonicalExpectedEventId => {
    305                 formatter.write_str("expected event ID is not canonical lowercase hexadecimal")
    306             }
    307             Self::EventIdMismatch { declared, computed } => write!(
    308                 formatter,
    309                 "plan event ID mismatch: declared {declared}, computed {computed}"
    310             ),
    311             Self::PlanDigest(error) => write!(formatter, "invalid plan digest: {error}"),
    312             Self::PlanDigestMismatch { declared, computed } => write!(
    313                 formatter,
    314                 "plan digest mismatch: declared {declared}, computed {computed}"
    315             ),
    316         }
    317     }
    318 }
    319 
    320 #[cfg(feature = "std")]
    321 impl std::error::Error for PlanDecodeError {}
    322 
    323 #[cfg(feature = "json")]
    324 #[derive(serde::Deserialize)]
    325 #[serde(deny_unknown_fields)]
    326 struct RawPlanWireV1 {
    327     schema_version: u32,
    328     contract_registry_version: u32,
    329     contract_id: String,
    330     expected_author: String,
    331     created_at: u64,
    332     kind: u32,
    333     tags: Vec<Vec<String>>,
    334     content: String,
    335     expected_event_id: String,
    336     plan_digest: String,
    337 }
    338 
    339 #[cfg(feature = "serde")]
    340 impl serde::Serialize for PlanWireV1 {
    341     fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    342     where
    343         S: serde::Serializer,
    344     {
    345         use serde::ser::SerializeStruct;
    346 
    347         let mut state = serializer.serialize_struct("PlanWireV1", 10)?;
    348         state.serialize_field("schema_version", &self.schema_version)?;
    349         state.serialize_field(
    350             "contract_registry_version",
    351             &self.contract.registry_version().get(),
    352         )?;
    353         state.serialize_field("contract_id", self.contract.contract_id().as_str())?;
    354         state.serialize_field("expected_author", &self.expected_author.to_hex())?;
    355         state.serialize_field("created_at", &self.created_at)?;
    356         state.serialize_field("kind", &self.kind)?;
    357         state.serialize_field("tags", &self.tags)?;
    358         state.serialize_field("content", &self.content)?;
    359         state.serialize_field("expected_event_id", &self.expected_event_id.to_hex())?;
    360         state.serialize_field("plan_digest", &self.plan_digest.to_hex())?;
    361         state.end()
    362     }
    363 }
    364 
    365 #[cfg(test)]
    366 mod tests {
    367     use super::*;
    368     use radroots_event::{GenericEventDraft, envelope::kind::KIND_GEOCHAT};
    369     use serde_json::{Value, json};
    370 
    371     const ALICE: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
    372 
    373     fn plan() -> AuthoredEventPlan {
    374         AuthoredEventPlan::from_generic(
    375             GenericEventDraft::new(
    376                 "radroots.social.geochat.v1",
    377                 KIND_GEOCHAT,
    378                 1_700_000_000,
    379                 vec![vec!["g".to_owned(), "u4pru".to_owned()]],
    380                 "hello 🍓",
    381                 ALICE,
    382             )
    383             .expect("generic draft"),
    384         )
    385         .expect("authored plan")
    386     }
    387 
    388     fn wire_json() -> Vec<u8> {
    389         PlanWireV1::from_plan(&plan()).to_json().expect("wire JSON")
    390     }
    391 
    392     fn mutate(mutator: impl FnOnce(&mut Value)) -> Vec<u8> {
    393         let mut value = serde_json::from_slice::<Value>(&wire_json()).expect("wire value");
    394         mutator(&mut value);
    395         serde_json::to_vec(&value).expect("mutated wire")
    396     }
    397 
    398     #[test]
    399     fn valid_plan_round_trips_with_exact_ordered_storage_wire() {
    400         let plan = plan();
    401         let wire = PlanWireV1::from_plan(&plan);
    402         let json = wire.to_json().expect("wire JSON");
    403         let expected = format!(
    404             concat!(
    405                 "{{\"schema_version\":1,",
    406                 "\"contract_registry_version\":7,",
    407                 "\"contract_id\":\"radroots.social.geochat.v1\",",
    408                 "\"expected_author\":\"{}\",",
    409                 "\"created_at\":1700000000,",
    410                 "\"kind\":20000,",
    411                 "\"tags\":[[\"g\",\"u4pru\"]],",
    412                 "\"content\":\"hello 🍓\",",
    413                 "\"expected_event_id\":\"{}\",",
    414                 "\"plan_digest\":\"{}\"}}"
    415             ),
    416             ALICE,
    417             plan.expected_event_id().to_hex(),
    418             plan.digest().to_hex(),
    419         );
    420         assert_eq!(String::from_utf8(json.clone()).expect("UTF-8"), expected);
    421 
    422         let integrity = PlanWireV1::from_json(&json).expect("historically valid plan");
    423         assert_eq!(integrity.plan(), &plan);
    424         assert_eq!(
    425             integrity.registry_relation(RegistryVersion::CURRENT),
    426             PlanRegistryRelation::Current
    427         );
    428         assert_eq!(
    429             integrity.registry_relation(RegistryVersion::new(8).expect("simulated advance")),
    430             PlanRegistryRelation::Historical
    431         );
    432         assert_eq!(PlanWireV1::from_plan(integrity.plan()), wire);
    433     }
    434 
    435     #[test]
    436     fn unknown_duplicate_oversize_and_unsupported_versions_fail_closed() {
    437         let unknown = mutate(|value| value["unknown"] = json!(true));
    438         assert!(matches!(
    439             PlanWireV1::from_json(&unknown),
    440             Err(PlanDecodeError::Json(_))
    441         ));
    442 
    443         let json = String::from_utf8(wire_json()).expect("UTF-8");
    444         let duplicate = json.replacen(
    445             "{\"schema_version\":1,",
    446             "{\"schema_version\":1,\"schema_version\":1,",
    447             1,
    448         );
    449         assert!(matches!(
    450             PlanWireV1::from_json(duplicate.as_bytes()),
    451             Err(PlanDecodeError::Json(_))
    452         ));
    453         assert_eq!(
    454             PlanWireV1::from_json(&vec![b' '; PLAN_WIRE_MAX_BYTES + 1]),
    455             Err(PlanDecodeError::RawJsonTooLarge {
    456                 max: PLAN_WIRE_MAX_BYTES,
    457                 actual: PLAN_WIRE_MAX_BYTES + 1,
    458             })
    459         );
    460 
    461         let schema = mutate(|value| value["schema_version"] = json!(2));
    462         assert_eq!(
    463             PlanWireV1::from_json(&schema),
    464             Err(PlanDecodeError::UnsupportedSchemaVersion { actual: 2 })
    465         );
    466         let registry = mutate(|value| value["contract_registry_version"] = json!(8));
    467         assert!(matches!(
    468             PlanWireV1::from_json(&registry),
    469             Err(PlanDecodeError::ContractIdentity(
    470                 ContractIdentityError::UnsupportedRegistryVersion { actual: 8 }
    471             ))
    472         ));
    473     }
    474 
    475     #[test]
    476     fn stale_or_tampered_commitments_and_noncanonical_identifiers_fail_closed() {
    477         let content = mutate(|value| value["content"] = json!("tampered"));
    478         assert!(matches!(
    479             PlanWireV1::from_json(&content),
    480             Err(PlanDecodeError::EventIdMismatch { .. })
    481         ));
    482         let id = mutate(|value| {
    483             value["expected_event_id"] =
    484                 json!("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
    485         });
    486         assert!(matches!(
    487             PlanWireV1::from_json(&id),
    488             Err(PlanDecodeError::EventIdMismatch { .. })
    489         ));
    490         let digest = mutate(|value| {
    491             value["plan_digest"] =
    492                 json!("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
    493         });
    494         assert!(matches!(
    495             PlanWireV1::from_json(&digest),
    496             Err(PlanDecodeError::PlanDigestMismatch { .. })
    497         ));
    498         let uppercase_digest = mutate(|value| {
    499             value["plan_digest"] = json!(plan().digest().to_hex().to_uppercase());
    500         });
    501         assert_eq!(
    502             PlanWireV1::from_json(&uppercase_digest),
    503             Err(PlanDecodeError::PlanDigest(PlanDigestError))
    504         );
    505         let kind = mutate(|value| value["kind"] = json!(1));
    506         assert_eq!(
    507             PlanWireV1::from_json(&kind),
    508             Err(PlanDecodeError::ContractKindMismatch {
    509                 expected: KIND_GEOCHAT,
    510                 actual: 1,
    511             })
    512         );
    513     }
    514 
    515     #[test]
    516     fn typed_contract_rejects_a_shape_from_the_wrong_historical_profile() {
    517         let typed = mutate(|value| {
    518             value["contract_id"] = json!("radroots.social.update.v1");
    519             value["kind"] = json!(1);
    520             value["tags"] = json!([["g", "u4pru"]]);
    521             value["content"] = json!("hello");
    522         });
    523         assert_eq!(
    524             PlanWireV1::from_json(&typed),
    525             Err(PlanDecodeError::HistoricalShape(
    526                 "update_tags_forbidden".to_owned()
    527             ))
    528         );
    529     }
    530 
    531     #[test]
    532     fn adversarial_truncation_and_single_byte_mutation_never_panic() {
    533         let wire = wire_json();
    534         for end in 0..wire.len() {
    535             let _ = PlanWireV1::from_json(&wire[..end]);
    536         }
    537         for index in 0..wire.len() {
    538             let mut mutated = wire.clone();
    539             mutated[index] = b'?';
    540             let _ = PlanWireV1::from_json(&mutated);
    541         }
    542     }
    543 }