commit 9a8486d138fc612d4d94693ca6b61b0203d375c5
parent f2fa280846d814595e02ad924d10ed39f7460a8e
Author: triesap <tyson@radroots.org>
Date: Tue, 4 Aug 2026 23:31:53 +0000
feat(event_codec): add historical plan wire
- freeze registry-v7 contract dispatch independently of current policy
- add bounded validating plan wire reconstruction and semantic integrity proof
- reject malformed stale tampered and unsupported stored plan representations
- lock package-mirrored conformance and adversarial parser coverage
Diffstat:
7 files changed, 765 insertions(+), 10 deletions(-)
diff --git a/contracts/conformance/vectors/event/authored_plan_wire.v1.json b/contracts/conformance/vectors/event/authored_plan_wire.v1.json
@@ -0,0 +1,40 @@
+{
+ "suite": "authored_plan_wire",
+ "contract_version": "1.0.0",
+ "vectors": [
+ {
+ "id": "generic_geochat_plan_wire_001",
+ "kind": "authored_plan_wire.v1",
+ "input": {
+ "contract_id": "radroots.social.geochat.v1",
+ "expected_author": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "created_at": 1700000000,
+ "kind": 20000,
+ "tags": [
+ [
+ "g",
+ "u4pru"
+ ]
+ ],
+ "content": "hello 🍓"
+ },
+ "expected": {
+ "schema_version": 1,
+ "contract_registry_version": 7,
+ "contract_id": "radroots.social.geochat.v1",
+ "expected_author": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "created_at": 1700000000,
+ "kind": 20000,
+ "tags": [
+ [
+ "g",
+ "u4pru"
+ ]
+ ],
+ "content": "hello 🍓",
+ "expected_event_id": "1623d687ab4f1c27f0b7eebe6e5fc0d3014f94f79c59d36a0ce456805be1e24a",
+ "plan_digest": "eb719fe792ef467b35810967c28a7fdc1c2fb8cebaefdf76efa878fe6dd4f1ec"
+ }
+ }
+ ]
+}
diff --git a/crates/event/src/contract.rs b/crates/event/src/contract.rs
@@ -84,6 +84,8 @@ impl fmt::Display for ContractId {
pub struct RegistryVersion(u32);
impl RegistryVersion {
+ /// Frozen registry profile used by the initial authored-plan storage wire.
+ pub const V7: Self = Self(7);
pub const CURRENT: Self = Self(RADROOTS_EVENT_CONTRACT_REGISTRY_VERSION);
pub const fn new(value: u32) -> Result<Self, ContractIdentityError> {
@@ -121,12 +123,15 @@ impl ContractKey {
registry_version: RegistryVersion,
contract_id: ContractId,
) -> Result<Self, ContractIdentityError> {
- if registry_version != RegistryVersion::CURRENT {
- return Err(ContractIdentityError::UnsupportedRegistryVersion {
- actual: registry_version.get(),
- });
- }
- if event_contract_registry_v7(contract_id.as_str()).is_none() {
+ let contract = match registry_version {
+ RegistryVersion::V7 => event_contract_registry_v7(contract_id.as_str()),
+ _ => {
+ return Err(ContractIdentityError::UnsupportedRegistryVersion {
+ actual: registry_version.get(),
+ });
+ }
+ };
+ if contract.is_none() {
return Err(ContractIdentityError::UnknownContract {
contract_id: contract_id.into_string(),
});
@@ -149,8 +154,11 @@ impl ContractKey {
#[must_use]
pub fn contract(&self) -> &'static EventContract {
- event_contract_registry_v7(self.contract_id.as_str())
- .expect("validated registry-v7 contract key must remain resolvable")
+ match self.registry_version {
+ RegistryVersion::V7 => event_contract_registry_v7(self.contract_id.as_str())
+ .expect("validated registry-v7 contract key must remain resolvable"),
+ _ => unreachable!("validated contract key must have a supported registry profile"),
+ }
}
}
@@ -298,6 +306,7 @@ mod identity_tests {
let id = ContractId::parse("radroots.social.geochat.v1").expect("contract ID");
assert_eq!(id.as_str(), "radroots.social.geochat.v1");
assert_eq!(RegistryVersion::CURRENT.get(), 7);
+ assert_eq!(RegistryVersion::V7.get(), 7);
let key = ContractKey::new(RegistryVersion::CURRENT, id).expect("contract key");
assert_eq!(key.contract().id, "radroots.social.geochat.v1");
diff --git a/crates/event_codec/src/authoring/mod.rs b/crates/event_codec/src/authoring/mod.rs
@@ -5,6 +5,7 @@ use alloc::{borrow::ToOwned, string::String, vec::Vec};
#[cfg(feature = "std")]
use std::{borrow::ToOwned, string::String, vec::Vec};
+use core::fmt;
use radroots_event::{GenericEventDraft, contract::ContractKey, draft::DraftError, id::EventId};
use radroots_identity::PublicKey;
use sha2::{Digest, Sha256};
@@ -12,6 +13,12 @@ use sha2::{Digest, Sha256};
pub const PLAN_WIRE_VERSION_V1: u32 = 1;
const PLAN_DIGEST_DOMAIN: &[u8] = b"radroots.authored_event_plan.v1";
+mod wire;
+
+pub use wire::{
+ HistoricalPlanIntegrity, PLAN_WIRE_MAX_BYTES, PlanDecodeError, PlanRegistryRelation, PlanWireV1,
+};
+
/// Exact contract-owned NIP-01 payload fields before author/time binding.
///
/// Fields are private and there is deliberately no arbitrary-parts
@@ -83,8 +90,33 @@ impl PlanDigest {
pub fn to_hex(self) -> String {
hex::encode(self.0)
}
+
+ pub fn parse_hex(value: &str) -> Result<Self, PlanDigestError> {
+ if value.len() != Self::BYTE_LENGTH * 2
+ || !value
+ .bytes()
+ .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
+ {
+ return Err(PlanDigestError);
+ }
+ let mut bytes = [0_u8; Self::BYTE_LENGTH];
+ hex::decode_to_slice(value, &mut bytes).map_err(|_| PlanDigestError)?;
+ Ok(Self(bytes))
+ }
}
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub struct PlanDigestError;
+
+impl fmt::Display for PlanDigestError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ formatter.write_str("plan digest must be exactly 64 lowercase hexadecimal characters")
+ }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for PlanDigestError {}
+
/// One immutable exact NIP-01 event plan, ready for authorization and signing.
///
/// ```compile_fail
@@ -117,7 +149,7 @@ impl AuthoredEventPlan {
))
}
- fn from_validated_parts(
+ pub(super) fn from_validated_parts(
body: AuthoredEventBody,
author: PublicKey,
created_at: u64,
diff --git a/crates/event_codec/src/authoring/wire.rs b/crates/event_codec/src/authoring/wire.rs
@@ -0,0 +1,531 @@
+//! Bounded, proof-carrying storage wire for authored event plans.
+
+#[cfg(all(not(feature = "std"), feature = "json"))]
+use alloc::string::ToString;
+#[cfg(not(feature = "std"))]
+use alloc::{borrow::ToOwned, string::String, vec::Vec};
+#[cfg(feature = "std")]
+use std::{borrow::ToOwned, string::String, vec::Vec};
+
+use core::fmt;
+#[cfg(feature = "json")]
+use radroots_event::{
+ contract::{ContractId, EventAuthoringPolicy, validate_event_contract_parts},
+ envelope::EventTags,
+ wire::compute_canonical_nip01_event_id,
+};
+use radroots_event::{
+ contract::{ContractIdentityError, ContractKey, RegistryVersion},
+ id::EventId,
+ wire::v1::DEFAULT_RAW_JSON_MAX_BYTES,
+};
+use radroots_identity::PublicKey;
+
+#[cfg(feature = "json")]
+use super::AuthoredEventBody;
+use super::{AuthoredEventPlan, PLAN_WIRE_VERSION_V1, PlanDigest, PlanDigestError};
+
+/// Hard byte limit applied before JSON parsing or field allocation.
+pub const PLAN_WIRE_MAX_BYTES: usize = DEFAULT_RAW_JSON_MAX_BYTES;
+
+/// Exact version-one durable representation of an authored event plan.
+///
+/// Construction is limited to validated plans. Deserialization is intentionally
+/// implemented only through [`Self::from_json`], which validates the embedded
+/// historical registry profile and both cryptographic commitments.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct PlanWireV1 {
+ schema_version: u32,
+ contract: ContractKey,
+ expected_author: PublicKey,
+ created_at: u64,
+ kind: u32,
+ tags: Vec<Vec<String>>,
+ content: String,
+ expected_event_id: EventId,
+ plan_digest: PlanDigest,
+}
+
+impl PlanWireV1 {
+ #[must_use]
+ pub fn from_plan(plan: &AuthoredEventPlan) -> Self {
+ Self {
+ schema_version: PLAN_WIRE_VERSION_V1,
+ contract: plan.body().contract().clone(),
+ expected_author: *plan.author(),
+ created_at: plan.created_at(),
+ kind: plan.body().kind(),
+ tags: plan.body().tags().to_vec(),
+ content: plan.body().content().to_owned(),
+ expected_event_id: *plan.expected_event_id(),
+ plan_digest: plan.digest(),
+ }
+ }
+
+ #[must_use]
+ pub const fn schema_version(&self) -> u32 {
+ self.schema_version
+ }
+
+ #[must_use]
+ pub const fn contract(&self) -> &ContractKey {
+ &self.contract
+ }
+
+ #[must_use]
+ pub const fn expected_author(&self) -> &PublicKey {
+ &self.expected_author
+ }
+
+ #[must_use]
+ pub const fn created_at(&self) -> u64 {
+ self.created_at
+ }
+
+ #[must_use]
+ pub const fn kind(&self) -> u32 {
+ self.kind
+ }
+
+ #[must_use]
+ pub fn tags(&self) -> &[Vec<String>] {
+ &self.tags
+ }
+
+ #[must_use]
+ pub fn content(&self) -> &str {
+ &self.content
+ }
+
+ #[must_use]
+ pub const fn expected_event_id(&self) -> &EventId {
+ &self.expected_event_id
+ }
+
+ #[must_use]
+ pub const fn plan_digest(&self) -> PlanDigest {
+ self.plan_digest
+ }
+
+ #[cfg(feature = "json")]
+ pub fn to_json(&self) -> Result<Vec<u8>, PlanDecodeError> {
+ serde_json::to_vec(self).map_err(|error| PlanDecodeError::Json(error.to_string()))
+ }
+
+ #[cfg(feature = "json")]
+ pub fn from_json(bytes: &[u8]) -> Result<HistoricalPlanIntegrity, PlanDecodeError> {
+ if bytes.len() > PLAN_WIRE_MAX_BYTES {
+ return Err(PlanDecodeError::RawJsonTooLarge {
+ max: PLAN_WIRE_MAX_BYTES,
+ actual: bytes.len(),
+ });
+ }
+ let wire = serde_json::from_slice::<RawPlanWireV1>(bytes)
+ .map_err(|error| PlanDecodeError::Json(error.to_string()))?;
+ Self::validate_raw(wire)
+ }
+
+ #[cfg(feature = "json")]
+ fn validate_raw(wire: RawPlanWireV1) -> Result<HistoricalPlanIntegrity, PlanDecodeError> {
+ if wire.schema_version != PLAN_WIRE_VERSION_V1 {
+ return Err(PlanDecodeError::UnsupportedSchemaVersion {
+ actual: wire.schema_version,
+ });
+ }
+
+ let registry_version = RegistryVersion::new(wire.contract_registry_version)
+ .map_err(PlanDecodeError::ContractIdentity)?;
+ let contract_id =
+ ContractId::parse(wire.contract_id).map_err(PlanDecodeError::ContractIdentity)?;
+ let contract = ContractKey::new(registry_version, contract_id)
+ .map_err(PlanDecodeError::ContractIdentity)?;
+ let definition = contract.contract();
+ if definition.kind != wire.kind {
+ return Err(PlanDecodeError::ContractKindMismatch {
+ expected: definition.kind,
+ actual: wire.kind,
+ });
+ }
+ if definition.authoring_policy() != EventAuthoringPolicy::GenericDraft {
+ return Err(PlanDecodeError::HistoricalProfileUnavailable {
+ contract_id: definition.id.to_owned(),
+ });
+ }
+
+ validate_event_contract_parts(wire.kind, &wire.tags, &wire.content, definition.id)
+ .map_err(|error| PlanDecodeError::HistoricalShape(error.code().to_owned()))?;
+ EventTags::new(wire.tags.clone())
+ .map_err(|error| PlanDecodeError::HistoricalShape(error.to_string()))?;
+ let expected_author = PublicKey::from_hex(&wire.expected_author)
+ .map_err(|error| PlanDecodeError::ExpectedAuthor(error.to_string()))?;
+ if expected_author.to_hex() != wire.expected_author {
+ return Err(PlanDecodeError::NonCanonicalExpectedAuthor);
+ }
+ let expected_event_id = EventId::parse(&wire.expected_event_id)
+ .map_err(|error| PlanDecodeError::ExpectedEventId(error.to_string()))?;
+ if expected_event_id.to_hex() != wire.expected_event_id {
+ return Err(PlanDecodeError::NonCanonicalExpectedEventId);
+ }
+ let recomputed_event_id = compute_canonical_nip01_event_id(
+ &wire.expected_author,
+ wire.created_at,
+ wire.kind,
+ &wire.tags,
+ &wire.content,
+ )
+ .map_err(|error| PlanDecodeError::ExpectedEventId(error.to_string()))?;
+ if recomputed_event_id != expected_event_id {
+ return Err(PlanDecodeError::EventIdMismatch {
+ declared: expected_event_id.to_hex(),
+ computed: recomputed_event_id.to_hex(),
+ });
+ }
+
+ let declared_digest =
+ PlanDigest::parse_hex(&wire.plan_digest).map_err(PlanDecodeError::PlanDigest)?;
+ let body = AuthoredEventBody {
+ contract: contract.clone(),
+ kind: wire.kind,
+ tags: wire.tags,
+ content: wire.content,
+ };
+ let plan = AuthoredEventPlan::from_validated_parts(
+ body,
+ expected_author,
+ wire.created_at,
+ expected_event_id,
+ );
+ if plan.digest() != declared_digest {
+ return Err(PlanDecodeError::PlanDigestMismatch {
+ declared: declared_digest.to_hex(),
+ computed: plan.digest().to_hex(),
+ });
+ }
+
+ Ok(HistoricalPlanIntegrity { plan })
+ }
+}
+
+/// Proof that a plan reconstructed successfully under its embedded registry.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct HistoricalPlanIntegrity {
+ plan: AuthoredEventPlan,
+}
+
+impl HistoricalPlanIntegrity {
+ #[must_use]
+ pub const fn plan(&self) -> &AuthoredEventPlan {
+ &self.plan
+ }
+
+ #[must_use]
+ pub fn into_plan(self) -> AuthoredEventPlan {
+ self.plan
+ }
+
+ /// Compares registry versions for policy reporting only.
+ ///
+ /// This relation is not current signing authorization. Signing must obtain
+ /// an explicit authorization decision from `radroots_signing`.
+ #[must_use]
+ pub fn registry_relation(&self, current: RegistryVersion) -> PlanRegistryRelation {
+ if self.plan.body().contract().registry_version() == current {
+ PlanRegistryRelation::Current
+ } else {
+ PlanRegistryRelation::Historical
+ }
+ }
+}
+
+/// Informational relation between an intact historical plan and current policy.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum PlanRegistryRelation {
+ Current,
+ Historical,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum PlanDecodeError {
+ RawJsonTooLarge { max: usize, actual: usize },
+ Json(String),
+ UnsupportedSchemaVersion { actual: u32 },
+ ContractIdentity(ContractIdentityError),
+ ContractKindMismatch { expected: u32, actual: u32 },
+ HistoricalProfileUnavailable { contract_id: String },
+ HistoricalShape(String),
+ ExpectedAuthor(String),
+ NonCanonicalExpectedAuthor,
+ ExpectedEventId(String),
+ NonCanonicalExpectedEventId,
+ EventIdMismatch { declared: String, computed: String },
+ PlanDigest(PlanDigestError),
+ PlanDigestMismatch { declared: String, computed: String },
+}
+
+impl fmt::Display for PlanDecodeError {
+ fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::RawJsonTooLarge { max, actual } => {
+ write!(formatter, "plan wire is {actual} bytes; max is {max}")
+ }
+ Self::Json(error) => write!(formatter, "plan wire JSON is invalid: {error}"),
+ Self::UnsupportedSchemaVersion { actual } => {
+ write!(formatter, "unsupported plan wire schema version {actual}")
+ }
+ Self::ContractIdentity(error) => write!(formatter, "invalid contract key: {error}"),
+ Self::ContractKindMismatch { expected, actual } => write!(
+ formatter,
+ "plan kind {actual} does not match historical contract kind {expected}"
+ ),
+ Self::HistoricalProfileUnavailable { contract_id } => write!(
+ formatter,
+ "historical authoring profile unavailable for `{contract_id}`"
+ ),
+ Self::HistoricalShape(error) => {
+ write!(formatter, "historical plan shape is invalid: {error}")
+ }
+ Self::ExpectedAuthor(error) => write!(formatter, "invalid expected author: {error}"),
+ Self::NonCanonicalExpectedAuthor => {
+ formatter.write_str("expected author is not canonical lowercase hexadecimal")
+ }
+ Self::ExpectedEventId(error) => write!(formatter, "invalid expected event ID: {error}"),
+ Self::NonCanonicalExpectedEventId => {
+ formatter.write_str("expected event ID is not canonical lowercase hexadecimal")
+ }
+ Self::EventIdMismatch { declared, computed } => write!(
+ formatter,
+ "plan event ID mismatch: declared {declared}, computed {computed}"
+ ),
+ Self::PlanDigest(error) => write!(formatter, "invalid plan digest: {error}"),
+ Self::PlanDigestMismatch { declared, computed } => write!(
+ formatter,
+ "plan digest mismatch: declared {declared}, computed {computed}"
+ ),
+ }
+ }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for PlanDecodeError {}
+
+#[cfg(feature = "json")]
+#[derive(serde::Deserialize)]
+#[serde(deny_unknown_fields)]
+struct RawPlanWireV1 {
+ schema_version: u32,
+ contract_registry_version: u32,
+ contract_id: String,
+ expected_author: String,
+ created_at: u64,
+ kind: u32,
+ tags: Vec<Vec<String>>,
+ content: String,
+ expected_event_id: String,
+ plan_digest: String,
+}
+
+#[cfg(feature = "serde")]
+impl serde::Serialize for PlanWireV1 {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ use serde::ser::SerializeStruct;
+
+ let mut state = serializer.serialize_struct("PlanWireV1", 10)?;
+ state.serialize_field("schema_version", &self.schema_version)?;
+ state.serialize_field(
+ "contract_registry_version",
+ &self.contract.registry_version().get(),
+ )?;
+ state.serialize_field("contract_id", self.contract.contract_id().as_str())?;
+ state.serialize_field("expected_author", &self.expected_author.to_hex())?;
+ state.serialize_field("created_at", &self.created_at)?;
+ state.serialize_field("kind", &self.kind)?;
+ state.serialize_field("tags", &self.tags)?;
+ state.serialize_field("content", &self.content)?;
+ state.serialize_field("expected_event_id", &self.expected_event_id.to_hex())?;
+ state.serialize_field("plan_digest", &self.plan_digest.to_hex())?;
+ state.end()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use radroots_event::{GenericEventDraft, envelope::kind::KIND_GEOCHAT};
+ use serde_json::{Value, json};
+
+ const ALICE: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
+
+ fn plan() -> AuthoredEventPlan {
+ AuthoredEventPlan::from_generic(
+ GenericEventDraft::new(
+ "radroots.social.geochat.v1",
+ KIND_GEOCHAT,
+ 1_700_000_000,
+ vec![vec!["g".to_owned(), "u4pru".to_owned()]],
+ "hello 🍓",
+ ALICE,
+ )
+ .expect("generic draft"),
+ )
+ .expect("authored plan")
+ }
+
+ fn wire_json() -> Vec<u8> {
+ PlanWireV1::from_plan(&plan()).to_json().expect("wire JSON")
+ }
+
+ fn mutate(mutator: impl FnOnce(&mut Value)) -> Vec<u8> {
+ let mut value = serde_json::from_slice::<Value>(&wire_json()).expect("wire value");
+ mutator(&mut value);
+ serde_json::to_vec(&value).expect("mutated wire")
+ }
+
+ #[test]
+ fn valid_plan_round_trips_with_exact_ordered_storage_wire() {
+ let plan = plan();
+ let wire = PlanWireV1::from_plan(&plan);
+ let json = wire.to_json().expect("wire JSON");
+ let expected = format!(
+ concat!(
+ "{{\"schema_version\":1,",
+ "\"contract_registry_version\":7,",
+ "\"contract_id\":\"radroots.social.geochat.v1\",",
+ "\"expected_author\":\"{}\",",
+ "\"created_at\":1700000000,",
+ "\"kind\":20000,",
+ "\"tags\":[[\"g\",\"u4pru\"]],",
+ "\"content\":\"hello 🍓\",",
+ "\"expected_event_id\":\"{}\",",
+ "\"plan_digest\":\"{}\"}}"
+ ),
+ ALICE,
+ plan.expected_event_id().to_hex(),
+ plan.digest().to_hex(),
+ );
+ assert_eq!(String::from_utf8(json.clone()).expect("UTF-8"), expected);
+
+ let integrity = PlanWireV1::from_json(&json).expect("historically valid plan");
+ assert_eq!(integrity.plan(), &plan);
+ assert_eq!(
+ integrity.registry_relation(RegistryVersion::CURRENT),
+ PlanRegistryRelation::Current
+ );
+ assert_eq!(
+ integrity.registry_relation(RegistryVersion::new(8).expect("simulated advance")),
+ PlanRegistryRelation::Historical
+ );
+ assert_eq!(PlanWireV1::from_plan(integrity.plan()), wire);
+ }
+
+ #[test]
+ fn unknown_duplicate_oversize_and_unsupported_versions_fail_closed() {
+ let unknown = mutate(|value| value["unknown"] = json!(true));
+ assert!(matches!(
+ PlanWireV1::from_json(&unknown),
+ Err(PlanDecodeError::Json(_))
+ ));
+
+ let json = String::from_utf8(wire_json()).expect("UTF-8");
+ let duplicate = json.replacen(
+ "{\"schema_version\":1,",
+ "{\"schema_version\":1,\"schema_version\":1,",
+ 1,
+ );
+ assert!(matches!(
+ PlanWireV1::from_json(duplicate.as_bytes()),
+ Err(PlanDecodeError::Json(_))
+ ));
+ assert_eq!(
+ PlanWireV1::from_json(&vec![b' '; PLAN_WIRE_MAX_BYTES + 1]),
+ Err(PlanDecodeError::RawJsonTooLarge {
+ max: PLAN_WIRE_MAX_BYTES,
+ actual: PLAN_WIRE_MAX_BYTES + 1,
+ })
+ );
+
+ let schema = mutate(|value| value["schema_version"] = json!(2));
+ assert_eq!(
+ PlanWireV1::from_json(&schema),
+ Err(PlanDecodeError::UnsupportedSchemaVersion { actual: 2 })
+ );
+ let registry = mutate(|value| value["contract_registry_version"] = json!(8));
+ assert!(matches!(
+ PlanWireV1::from_json(®istry),
+ Err(PlanDecodeError::ContractIdentity(
+ ContractIdentityError::UnsupportedRegistryVersion { actual: 8 }
+ ))
+ ));
+ }
+
+ #[test]
+ fn stale_or_tampered_commitments_and_noncanonical_identifiers_fail_closed() {
+ let content = mutate(|value| value["content"] = json!("tampered"));
+ assert!(matches!(
+ PlanWireV1::from_json(&content),
+ Err(PlanDecodeError::EventIdMismatch { .. })
+ ));
+ let id = mutate(|value| {
+ value["expected_event_id"] =
+ json!("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
+ });
+ assert!(matches!(
+ PlanWireV1::from_json(&id),
+ Err(PlanDecodeError::EventIdMismatch { .. })
+ ));
+ let digest = mutate(|value| {
+ value["plan_digest"] =
+ json!("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
+ });
+ assert!(matches!(
+ PlanWireV1::from_json(&digest),
+ Err(PlanDecodeError::PlanDigestMismatch { .. })
+ ));
+ let uppercase_digest = mutate(|value| {
+ value["plan_digest"] = json!(plan().digest().to_hex().to_uppercase());
+ });
+ assert_eq!(
+ PlanWireV1::from_json(&uppercase_digest),
+ Err(PlanDecodeError::PlanDigest(PlanDigestError))
+ );
+ let kind = mutate(|value| value["kind"] = json!(1));
+ assert_eq!(
+ PlanWireV1::from_json(&kind),
+ Err(PlanDecodeError::ContractKindMismatch {
+ expected: KIND_GEOCHAT,
+ actual: 1,
+ })
+ );
+ }
+
+ #[test]
+ fn typed_contract_cannot_enter_through_the_generic_historical_profile() {
+ let typed = mutate(|value| {
+ value["contract_id"] = json!("radroots.social.update.v1");
+ value["kind"] = json!(1);
+ value["tags"] = json!([]);
+ value["content"] = json!("hello");
+ });
+ assert_eq!(
+ PlanWireV1::from_json(&typed),
+ Err(PlanDecodeError::HistoricalProfileUnavailable {
+ contract_id: "radroots.social.update.v1".to_owned(),
+ })
+ );
+ }
+
+ #[test]
+ fn adversarial_truncation_and_single_byte_mutation_never_panic() {
+ let wire = wire_json();
+ for end in 0..wire.len() {
+ let _ = PlanWireV1::from_json(&wire[..end]);
+ }
+ for index in 0..wire.len() {
+ let mut mutated = wire.clone();
+ mutated[index] = b'?';
+ let _ = PlanWireV1::from_json(&mutated);
+ }
+ }
+}
diff --git a/crates/event_codec/tests/authored_plan_wire.rs b/crates/event_codec/tests/authored_plan_wire.rs
@@ -0,0 +1,99 @@
+#![cfg(feature = "json")]
+
+use std::{borrow::Cow, fs, path::Path};
+
+use radroots_event::GenericEventDraft;
+use radroots_event_codec::authoring::{AuthoredEventPlan, PlanWireV1};
+use serde::Deserialize;
+use serde_json::Value;
+
+const PACKAGED_VECTORS: &str = include_str!("fixtures/authored_plan_wire.v1.json");
+const WORKSPACE_VECTOR_PATH: &str =
+ "../../contracts/conformance/vectors/event/authored_plan_wire.v1.json";
+const WORKSPACE_CONTRACT_MARKER_PATH: &str = "../../contracts/manifest.toml";
+
+#[derive(Debug, Deserialize)]
+struct Suite {
+ suite: String,
+ contract_version: String,
+ vectors: Vec<Vector>,
+}
+
+#[derive(Debug, Deserialize)]
+struct Vector {
+ id: String,
+ kind: String,
+ input: Input,
+ expected: Value,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(deny_unknown_fields)]
+struct Input {
+ contract_id: String,
+ expected_author: String,
+ created_at: u64,
+ kind: u32,
+ tags: Vec<Vec<String>>,
+ content: String,
+}
+
+#[test]
+fn checked_in_plan_wire_vector_executes_and_is_package_self_contained() {
+ let vectors = conformance_vectors();
+ let suite: Suite = serde_json::from_str(&vectors).expect("plan wire vectors must parse");
+ assert_eq!(suite.suite, "authored_plan_wire");
+ assert_eq!(suite.contract_version, "1.0.0");
+ assert!(!suite.vectors.is_empty());
+
+ for vector in suite.vectors {
+ assert_eq!(vector.kind, "authored_plan_wire.v1", "{}", vector.id);
+ let draft = GenericEventDraft::new(
+ vector.input.contract_id,
+ vector.input.kind,
+ vector.input.created_at,
+ vector.input.tags,
+ vector.input.content,
+ vector.input.expected_author,
+ )
+ .unwrap_or_else(|error| panic!("{} draft failed: {error}", vector.id));
+ let plan = AuthoredEventPlan::from_generic(draft)
+ .unwrap_or_else(|error| panic!("{} plan failed: {error}", vector.id));
+ let encoded = PlanWireV1::from_plan(&plan)
+ .to_json()
+ .unwrap_or_else(|error| panic!("{} encode failed: {error}", vector.id));
+ assert_eq!(
+ serde_json::from_slice::<Value>(&encoded).expect("encoded value"),
+ vector.expected,
+ "{}",
+ vector.id
+ );
+ assert_eq!(
+ PlanWireV1::from_json(&encoded)
+ .unwrap_or_else(|error| panic!("{} decode failed: {error}", vector.id))
+ .into_plan(),
+ plan,
+ "{}",
+ vector.id
+ );
+ }
+}
+
+fn conformance_vectors() -> Cow<'static, str> {
+ let workspace_path = Path::new(env!("CARGO_MANIFEST_DIR")).join(WORKSPACE_VECTOR_PATH);
+ match fs::read_to_string(&workspace_path) {
+ Ok(canonical) => {
+ assert_eq!(canonical, PACKAGED_VECTORS);
+ Cow::Owned(canonical)
+ }
+ Err(error)
+ if error.kind() == std::io::ErrorKind::NotFound
+ && !Path::new(env!("CARGO_MANIFEST_DIR"))
+ .join(WORKSPACE_CONTRACT_MARKER_PATH)
+ .is_file() =>
+ {
+ Cow::Borrowed(PACKAGED_VECTORS)
+ }
+ Err(error) => panic!("failed to read {}: {error}", workspace_path.display()),
+ }
+}
diff --git a/crates/event_codec/tests/fixtures/authored_plan_wire.v1.json b/crates/event_codec/tests/fixtures/authored_plan_wire.v1.json
@@ -0,0 +1,40 @@
+{
+ "suite": "authored_plan_wire",
+ "contract_version": "1.0.0",
+ "vectors": [
+ {
+ "id": "generic_geochat_plan_wire_001",
+ "kind": "authored_plan_wire.v1",
+ "input": {
+ "contract_id": "radroots.social.geochat.v1",
+ "expected_author": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "created_at": 1700000000,
+ "kind": 20000,
+ "tags": [
+ [
+ "g",
+ "u4pru"
+ ]
+ ],
+ "content": "hello 🍓"
+ },
+ "expected": {
+ "schema_version": 1,
+ "contract_registry_version": 7,
+ "contract_id": "radroots.social.geochat.v1",
+ "expected_author": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
+ "created_at": 1700000000,
+ "kind": 20000,
+ "tags": [
+ [
+ "g",
+ "u4pru"
+ ]
+ ],
+ "content": "hello 🍓",
+ "expected_event_id": "1623d687ab4f1c27f0b7eebe6e5fc0d3014f94f79c59d36a0ce456805be1e24a",
+ "plan_digest": "eb719fe792ef467b35810967c28a7fdc1c2fb8cebaefdf76efa878fe6dd4f1ec"
+ }
+ }
+ ]
+}
diff --git a/tools/xtask/src/contract.rs b/tools/xtask/src/contract.rs
@@ -62,7 +62,7 @@ const REPLICA_CONTRACT_RELATIVE: &str = "contracts/replica.toml";
const REPLICA_CONTRACT_NAME: &str = "radroots_replica_contract";
const REPLICA_TRANSFER_CONSTANT: &str = "RADROOTS_REPLICA_TRANSFER_VERSION";
const REPLICA_TRANSFER_VERSION: u32 = 2;
-const CONFORMANCE_VECTOR_MIRRORS: [(&str, &str); 18] = [
+const CONFORMANCE_VECTOR_MIRRORS: [(&str, &str); 19] = [
(
"contracts/conformance/vectors/blossom/bud11_claims.v1.json",
"crates/blossom/tests/fixtures/bud11_claims.v1.json",
@@ -104,6 +104,10 @@ const CONFORMANCE_VECTOR_MIRRORS: [(&str, &str); 18] = [
"crates/event_codec/tests/fixtures/verified_admission.v1.json",
),
(
+ "contracts/conformance/vectors/event/authored_plan_wire.v1.json",
+ "crates/event_codec/tests/fixtures/authored_plan_wire.v1.json",
+ ),
+ (
"contracts/conformance/vectors/events/operational_listing_tags_full.v1.json",
"crates/event_codec/tests/fixtures/operational_listing_tags_full.v1.json",
),