commit 37681abac5bee1e1d92d1820299566545efa8dfa
parent e0a5e2ac1742e90daa5ee5bd2a41c60bbc17046b
Author: triesap <tyson@radroots.org>
Date: Tue, 28 Jul 2026 17:11:42 +0000
protocol: complete package conformance coverage
- Add executable language-neutral wire vectors across all protocol modules
- Prove generated schema and serialized type inventory completeness
- Cover no_std testing and forbid runtime dependency surface drift
- Account for macro-generated OperationId in generated DTO authority
Diffstat:
8 files changed, 488 insertions(+), 2 deletions(-)
diff --git a/contracts/codegen/protocol_v1.inventory.json b/contracts/codegen/protocol_v1.inventory.json
@@ -230,6 +230,10 @@
"kind": "struct"
},
{
+ "rust_path": "radroots_protocol::runtime::v1::OperationId",
+ "kind": "enum"
+ },
+ {
"rust_path": "radroots_protocol::runtime::v1::PrivacyEffect",
"kind": "enum"
},
diff --git a/contracts/codegen/protocol_v1.inventory.sha256 b/contracts/codegen/protocol_v1.inventory.sha256
@@ -1 +1 @@
-e1abc485a0c89c70f2be99f275c535e6b2df6e8abac312ef5c7925593d320bb6
+36d98bfc7a76f8ac9c0bd36f6775b8ae2024ff821593d76ceef57ebc559a331c
diff --git a/contracts/codegen/protocol_v1.toml b/contracts/codegen/protocol_v1.toml
@@ -23,3 +23,10 @@ path = "crates/protocol/src/radrootsd/transport_publish/v5.rs"
[[source]]
module = "runtime::v1"
path = "crates/protocol/src/runtime/v1.rs"
+
+[[macro_generated_type]]
+module = "runtime::v1"
+path = "crates/protocol/src/runtime/v1.rs"
+macro_name = "operation_ids"
+rust_name = "OperationId"
+kind = "enum"
diff --git a/contracts/conformance/vectors/protocol/wire_values.v1.json b/contracts/conformance/vectors/protocol/wire_values.v1.json
@@ -0,0 +1,153 @@
+{
+ "suite": "protocol_wire_values_v1",
+ "contract_version": "1.0.0",
+ "vectors": [
+ {
+ "id": "protocol_capability_transport_kind_001",
+ "kind": "protocol.capability.transport_kind",
+ "input": "reticulum",
+ "expected": "reticulum"
+ },
+ {
+ "id": "protocol_capability_reticulum_target_002",
+ "kind": "protocol.capability.reticulum_target",
+ "input": {
+ "destination": { "canonical": "a1b2c3d4" },
+ "mesh_scope": { "value": "farm.mesh-1" }
+ },
+ "expected": {
+ "destination": { "canonical": "a1b2c3d4" },
+ "mesh_scope": { "value": "farm.mesh-1" }
+ }
+ },
+ {
+ "id": "protocol_event_class_003",
+ "kind": "protocol.event.event_class",
+ "input": "unsigned_rumor",
+ "expected": "unsigned_rumor"
+ },
+ {
+ "id": "protocol_trade_state_004",
+ "kind": "protocol.event.trade_state",
+ "input": "agreed_pending_validation",
+ "expected": "agreed_pending_validation"
+ },
+ {
+ "id": "protocol_runtime_operation_id_005",
+ "kind": "protocol.runtime.operation_id",
+ "input": "sync.push",
+ "expected": "sync.push"
+ },
+ {
+ "id": "protocol_runtime_risk_006",
+ "kind": "protocol.runtime.risk",
+ "input": "critical",
+ "expected": "critical"
+ },
+ {
+ "id": "protocol_runtime_transport_route_007",
+ "kind": "protocol.runtime.transport_route",
+ "input": {
+ "local": true,
+ "nostr": true,
+ "reticulum": false,
+ "deliver": true,
+ "fetch": false,
+ "synchronize": true,
+ "diagnostics": false
+ },
+ "expected": {
+ "local": true,
+ "nostr": true,
+ "reticulum": false,
+ "deliver": true,
+ "fetch": false,
+ "synchronize": true,
+ "diagnostics": false
+ }
+ },
+ {
+ "id": "protocol_daemon_target_policy_008",
+ "kind": "protocol.radrootsd.target_policy",
+ "input": {
+ "kind": "nostr",
+ "source_policy": "explicit_only",
+ "relay_urls": []
+ },
+ "expected": {
+ "kind": "nostr",
+ "source_policy": "explicit_only",
+ "relay_urls": []
+ }
+ },
+ {
+ "id": "protocol_daemon_delivery_policy_009",
+ "kind": "protocol.radrootsd.delivery_policy",
+ "input": { "mode": "quorum", "quorum": 2 },
+ "expected": { "mode": "quorum", "quorum": 2 }
+ },
+ {
+ "id": "protocol_error_report_010",
+ "kind": "protocol.error.report",
+ "input": {
+ "schema_version": 1,
+ "code": "relay_rate_limited",
+ "class": "network",
+ "retryable": true,
+ "recovery_actions": ["retry_after_transport_failure"],
+ "operation_id": "sync.push",
+ "capability_id": "nostr",
+ "message": "Relay rate limit requires a later retry",
+ "details": []
+ },
+ "expected": {
+ "schema_version": 1,
+ "code": "relay_rate_limited",
+ "class": "network",
+ "retryable": true,
+ "recovery_actions": ["retry_after_transport_failure"],
+ "operation_id": "sync.push",
+ "capability_id": "nostr",
+ "message": "Relay rate limit requires a later retry",
+ "details": []
+ }
+ },
+ {
+ "id": "protocol_runtime_unknown_operation_011",
+ "kind": "protocol.runtime.operation_id",
+ "input": "runtime.unknown",
+ "expected_error_contains": "unknown operation id"
+ },
+ {
+ "id": "protocol_runtime_route_unknown_field_012",
+ "kind": "protocol.runtime.transport_route",
+ "input": {
+ "local": true,
+ "nostr": false,
+ "reticulum": false,
+ "deliver": false,
+ "fetch": false,
+ "synchronize": false,
+ "diagnostics": false,
+ "executor": "forbidden"
+ },
+ "expected_error_contains": "unknown field"
+ },
+ {
+ "id": "protocol_error_report_version_013",
+ "kind": "protocol.error.report",
+ "input": {
+ "schema_version": 2,
+ "code": "relay_rate_limited",
+ "class": "network",
+ "retryable": true,
+ "recovery_actions": ["retry_after_transport_failure"],
+ "operation_id": "sync.push",
+ "capability_id": "nostr",
+ "message": "Relay rate limit requires a later retry",
+ "details": []
+ },
+ "expected_error_contains": "unsupported error report schema version 2"
+ }
+ ]
+}
diff --git a/crates/protocol/src/lib.rs b/crates/protocol/src/lib.rs
@@ -5,6 +5,9 @@
extern crate alloc;
+#[cfg(all(test, not(feature = "std")))]
+extern crate std;
+
/// Versioned capability catalog contracts.
pub mod capability;
diff --git a/crates/protocol/tests/conformance.rs b/crates/protocol/tests/conformance.rs
@@ -0,0 +1,130 @@
+#![cfg(feature = "serde")]
+
+use std::collections::BTreeSet;
+
+use radroots_protocol::{
+ capability::v1::{ReticulumTarget, TransportKind},
+ error::v1::ErrorReport,
+ event::v1::{EventClass, TradeState},
+ radrootsd::transport_publish::v5::{DeliveryPolicy, TargetPolicy},
+ runtime::v1::{OperationId, Risk, TransportRoute},
+ schema::{ModuleVersion, protocol_v1_registry},
+};
+use serde::{Serialize, de::DeserializeOwned};
+use serde_json::Value;
+
+const WIRE_VECTORS: &str =
+ include_str!("../../../contracts/conformance/vectors/protocol/wire_values.v1.json");
+const GENERATED_INVENTORY: &str =
+ include_str!("../../../contracts/codegen/protocol_v1.inventory.json");
+
+#[test]
+fn language_neutral_wire_vectors_are_unique_and_executable() {
+ let document: Value = serde_json::from_str(WIRE_VECTORS).expect("protocol wire vectors");
+ assert_eq!(document["suite"], "protocol_wire_values_v1");
+ assert_eq!(document["contract_version"], "1.0.0");
+ let vectors = document["vectors"].as_array().expect("vector array");
+ let mut ids = BTreeSet::new();
+ for vector in vectors {
+ let id = vector["id"].as_str().expect("vector id");
+ assert!(ids.insert(id), "duplicate protocol vector id `{id}`");
+ let kind = vector["kind"].as_str().expect("vector kind");
+ let input = vector["input"].clone();
+ let result = match kind {
+ "protocol.capability.transport_kind" => execute::<TransportKind>(input),
+ "protocol.capability.reticulum_target" => execute::<ReticulumTarget>(input),
+ "protocol.event.event_class" => execute::<EventClass>(input),
+ "protocol.event.trade_state" => execute::<TradeState>(input),
+ "protocol.runtime.operation_id" => execute::<OperationId>(input),
+ "protocol.runtime.risk" => execute::<Risk>(input),
+ "protocol.runtime.transport_route" => execute::<TransportRoute>(input),
+ "protocol.radrootsd.target_policy" => execute::<TargetPolicy>(input),
+ "protocol.radrootsd.delivery_policy" => execute::<DeliveryPolicy>(input),
+ "protocol.error.report" => execute::<ErrorReport>(input),
+ other => panic!("unimplemented protocol vector kind `{other}`"),
+ };
+ match (
+ vector.get("expected"),
+ vector.get("expected_error_contains"),
+ result,
+ ) {
+ (Some(expected), None, Ok(actual)) => assert_eq!(&actual, expected, "vector `{id}`"),
+ (None, Some(expected), Err(error)) => assert!(
+ error.contains(expected.as_str().expect("error fragment")),
+ "vector `{id}` expected `{expected}`, found `{error}`"
+ ),
+ (Some(_), None, Err(error)) => panic!("vector `{id}` unexpectedly failed: {error}"),
+ (None, Some(_), Ok(actual)) => {
+ panic!("vector `{id}` unexpectedly succeeded: {actual}")
+ }
+ _ => panic!("vector `{id}` has an invalid expectation shape"),
+ }
+ }
+ assert_eq!(ids.len(), 13);
+}
+
+#[test]
+fn generated_inventory_is_complete_unique_and_matches_the_schema_registry() {
+ let inventory: Value =
+ serde_json::from_str(GENERATED_INVENTORY).expect("generated protocol inventory");
+ assert_eq!(inventory["schema_version"], 1);
+ assert_eq!(inventory["package"], "radroots_protocol");
+
+ let sources = inventory["sources"].as_array().expect("sources");
+ let actual_modules = sources
+ .iter()
+ .map(|source| source["module"].as_str().expect("source module"))
+ .collect::<BTreeSet<_>>();
+ let expected_modules = ModuleVersion::ALL
+ .iter()
+ .map(|module| module.path())
+ .collect::<BTreeSet<_>>();
+ assert_eq!(actual_modules, expected_modules);
+
+ let type_paths = sources
+ .iter()
+ .flat_map(|source| source["types"].as_array().expect("source types"))
+ .map(|item| item["rust_path"].as_str().expect("Rust path"))
+ .collect::<Vec<_>>();
+ assert_eq!(type_paths.len(), 54);
+ assert_eq!(
+ type_paths.iter().copied().collect::<BTreeSet<_>>().len(),
+ 54
+ );
+ assert!(type_paths.contains(&"radroots_protocol::runtime::v1::OperationId"));
+
+ let inventory_schemas = inventory["schemas"]
+ .as_array()
+ .expect("schemas")
+ .iter()
+ .map(|schema| {
+ (
+ schema["schema_id"].as_str().expect("schema id").to_owned(),
+ schema["module"].as_str().expect("schema module").to_owned(),
+ schema["generation"].as_u64().expect("generation") as u16,
+ )
+ })
+ .collect::<BTreeSet<_>>();
+ let registry_schemas = protocol_v1_registry()
+ .expect("protocol registry")
+ .descriptors()
+ .iter()
+ .map(|descriptor| {
+ (
+ descriptor.id().as_str().to_owned(),
+ descriptor.module().path().to_owned(),
+ descriptor.module().generation(),
+ )
+ })
+ .collect::<BTreeSet<_>>();
+ assert_eq!(inventory_schemas.len(), 121);
+ assert_eq!(inventory_schemas, registry_schemas);
+}
+
+fn execute<T>(input: Value) -> Result<Value, String>
+where
+ T: DeserializeOwned + Serialize,
+{
+ let decoded: T = serde_json::from_value(input).map_err(|error| error.to_string())?;
+ serde_json::to_value(decoded).map_err(|error| error.to_string())
+}
diff --git a/crates/protocol/tests/package_boundary.rs b/crates/protocol/tests/package_boundary.rs
@@ -7,6 +7,14 @@ const ERROR: &str = include_str!("../src/error.rs");
const EVENT: &str = include_str!("../src/event.rs");
const RADROOTSD: &str = include_str!("../src/radrootsd.rs");
const RUNTIME: &str = include_str!("../src/runtime.rs");
+const VERSIONED_SOURCES: &[&str] = &[
+ include_str!("../src/capability/v1.rs"),
+ include_str!("../src/error/v1.rs"),
+ include_str!("../src/event/v1.rs"),
+ include_str!("../src/radrootsd/transport_publish/v5.rs"),
+ include_str!("../src/runtime/v1.rs"),
+ include_str!("../src/schema.rs"),
+];
#[test]
fn manifest_has_final_identity_features_and_no_radroots_dependencies() {
@@ -58,6 +66,35 @@ fn crate_root_exposes_only_the_approved_versioned_skeleton() {
);
}
+#[test]
+fn public_contract_sources_exclude_runtime_dependencies_and_wrapper_traits() {
+ assert!(ROOT.contains("#![forbid(unsafe_code)]"));
+ for source in VERSIONED_SOURCES {
+ for forbidden in [
+ "dto_bindgen",
+ "wasm_bindgen",
+ "uniffi",
+ "tokio::",
+ "sqlx::",
+ "reqwest::",
+ "nostr::",
+ "std::fs",
+ "std::net",
+ "std::process",
+ "impl core::ops::Deref",
+ "impl std::ops::Deref",
+ "unsafe fn",
+ "unsafe impl",
+ "unsafe {",
+ ] {
+ assert!(
+ !source.contains(forbidden),
+ "protocol contract source contains forbidden surface `{forbidden}`"
+ );
+ }
+ }
+}
+
fn table_keys<'a>(manifest: &'a str, heading: &str) -> BTreeSet<&'a str> {
let Some((_, table)) = manifest.split_once(heading) else {
return BTreeSet::new();
diff --git a/tools/xtask/src/generate/protocol.rs b/tools/xtask/src/generate/protocol.rs
@@ -24,6 +24,13 @@ const EXPECTED_SOURCES: &[(&str, &str)] = &[
),
("runtime::v1", "crates/protocol/src/runtime/v1.rs"),
];
+const EXPECTED_MACRO_GENERATED_TYPES: &[(&str, &str, &str, &str, &str)] = &[(
+ "runtime::v1",
+ "crates/protocol/src/runtime/v1.rs",
+ "operation_ids",
+ "OperationId",
+ "enum",
+)];
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
@@ -34,6 +41,8 @@ struct Config {
inventory_path: String,
inventory_sha256_path: String,
source: Vec<SourceConfig>,
+ #[serde(default)]
+ macro_generated_type: Vec<MacroGeneratedTypeConfig>,
}
#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd)]
@@ -43,6 +52,16 @@ struct SourceConfig {
path: String,
}
+#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd)]
+#[serde(deny_unknown_fields)]
+struct MacroGeneratedTypeConfig {
+ module: String,
+ path: String,
+ macro_name: String,
+ rust_name: String,
+ kind: String,
+}
+
#[derive(Debug, Serialize)]
struct Inventory {
schema_version: u16,
@@ -142,6 +161,26 @@ fn validate_config(config: &Config) -> Result<(), String> {
{
return Err("protocol codegen modules must be unique".to_owned());
}
+
+ let mut macro_generated = config
+ .macro_generated_type
+ .iter()
+ .map(|item| {
+ (
+ item.module.as_str(),
+ item.path.as_str(),
+ item.macro_name.as_str(),
+ item.rust_name.as_str(),
+ item.kind.as_str(),
+ )
+ })
+ .collect::<Vec<_>>();
+ macro_generated.sort_unstable();
+ if macro_generated != EXPECTED_MACRO_GENERATED_TYPES {
+ return Err(format!(
+ "protocol macro-generated type inventory drifted: expected {EXPECTED_MACRO_GENERATED_TYPES:?}, found {macro_generated:?}"
+ ));
+ }
Ok(())
}
@@ -181,11 +220,33 @@ fn render_outputs(
source.path
)
})?;
+ let mut types = serialized_public_types(&source.module, &source.path, text)?;
+ for generated in config
+ .macro_generated_type
+ .iter()
+ .filter(|generated| generated.module == source.module && generated.path == source.path)
+ {
+ types.push(macro_generated_serialized_type(
+ generated,
+ source.path.as_str(),
+ text,
+ )?);
+ }
+ types.sort_by(|left, right| left.rust_path.cmp(&right.rust_path));
+ if types
+ .windows(2)
+ .any(|pair| pair[0].rust_path == pair[1].rust_path)
+ {
+ return Err(format!(
+ "protocol DTO source `{}` exposes duplicate serialized type inventory entries",
+ source.path
+ ));
+ }
inventories.push(SourceInventory {
module: source.module.clone(),
path: source.path.clone(),
sha256: sha256_hex(&bytes),
- types: serialized_public_types(&source.module, &source.path, text)?,
+ types,
});
}
@@ -226,6 +287,66 @@ fn render_outputs(
])
}
+fn macro_generated_serialized_type(
+ config: &MacroGeneratedTypeConfig,
+ source_path: &str,
+ source: &str,
+) -> Result<TypeInventory, String> {
+ let syntax = syn::parse_file(source)
+ .map_err(|error| format!("failed to parse protocol DTO source `{source_path}`: {error}"))?;
+ let definition = syntax.items.iter().find_map(|item| match item {
+ Item::Macro(item)
+ if item
+ .ident
+ .as_ref()
+ .is_some_and(|name| name == &config.macro_name) =>
+ {
+ Some(item.mac.tokens.to_string())
+ }
+ _ => None,
+ });
+ let Some(definition) = definition else {
+ return Err(format!(
+ "protocol DTO source `{source_path}` does not define configured macro `{}`",
+ config.macro_name
+ ));
+ };
+ let invoked = syntax.items.iter().any(|item| {
+ matches!(item, Item::Macro(item) if item.ident.is_none() && item.mac.path.is_ident(&config.macro_name))
+ });
+ if !invoked {
+ return Err(format!(
+ "protocol DTO source `{source_path}` does not invoke configured macro `{}`",
+ config.macro_name
+ ));
+ }
+ let compact_definition = definition.split_whitespace().collect::<String>();
+ for required in [
+ format!("pub{}{}", config.kind, config.rust_name),
+ format!("implserde::Serializefor{}", config.rust_name),
+ format!("impl<'de>serde::Deserialize<'de>for{}", config.rust_name),
+ ] {
+ if !compact_definition.contains(&required) {
+ return Err(format!(
+ "configured macro `{}` in `{source_path}` does not prove serialized public {} `{}`: missing `{required}`",
+ config.macro_name, config.kind, config.rust_name
+ ));
+ }
+ }
+ Ok(TypeInventory {
+ rust_path: format!("radroots_protocol::{}::{}", config.module, config.rust_name),
+ kind: match config.kind.as_str() {
+ "enum" => "enum",
+ "struct" => "struct",
+ other => {
+ return Err(format!(
+ "unsupported macro-generated protocol DTO kind `{other}`"
+ ));
+ }
+ },
+ })
+}
+
fn serialized_public_types(
module: &str,
source_path: &str,
@@ -463,6 +584,7 @@ struct PrivateWire;
module: "demo::v1".to_owned(),
path: "src/types.rs".to_owned(),
}],
+ macro_generated_type: Vec::new(),
};
let schemas = vec![SchemaInventory {
schema_id: "demo.message.v1".to_owned(),
@@ -487,4 +609,34 @@ struct PrivateWire;
assert!(safe_workspace_file(workspace.path(), path, true, "fixture").is_err());
}
}
+
+ #[test]
+ fn macro_generated_serialized_type_requires_definition_invocation_and_serde() {
+ let config = MacroGeneratedTypeConfig {
+ module: "demo::v1".to_owned(),
+ path: "demo.rs".to_owned(),
+ macro_name: "generated_ids".to_owned(),
+ rust_name: "GeneratedId".to_owned(),
+ kind: "enum".to_owned(),
+ };
+ let source = r#"
+macro_rules! generated_ids {
+ () => {
+ pub enum GeneratedId { One }
+ impl serde::Serialize for GeneratedId {}
+ impl<'de> serde::Deserialize<'de> for GeneratedId {}
+ };
+}
+generated_ids!();
+"#;
+ let item = macro_generated_serialized_type(&config, "demo.rs", source)
+ .expect("macro-generated serialized type");
+ assert_eq!(item.rust_path, "radroots_protocol::demo::v1::GeneratedId");
+ assert_eq!(item.kind, "enum");
+
+ let missing_serde = source.replace("impl serde::Serialize for GeneratedId {}", "");
+ let error = macro_generated_serialized_type(&config, "demo.rs", &missing_serde)
+ .expect_err("missing serializer must fail closed");
+ assert!(error.contains("does not prove serialized public enum `GeneratedId`"));
+ }
}