lib

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

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:
Mcontracts/codegen/protocol_v1.inventory.json | 4++++
Mcontracts/codegen/protocol_v1.inventory.sha256 | 2+-
Mcontracts/codegen/protocol_v1.toml | 7+++++++
Acontracts/conformance/vectors/protocol/wire_values.v1.json | 153+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/protocol/src/lib.rs | 3+++
Acrates/protocol/tests/conformance.rs | 130+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/protocol/tests/package_boundary.rs | 37+++++++++++++++++++++++++++++++++++++
Mtools/xtask/src/generate/protocol.rs | 154++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
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`")); + } }