lib

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

commit cb8fcfe1e058141220df6e3d33da4b81527595a7
parent 9b329721eae701e08c6056b9e0f3954306322d05
Author: triesap <tyson@radroots.org>
Date:   Mon, 27 Jul 2026 20:03:58 +0000

identity: complete package conformance coverage

- add governed public-value vectors and external account API tests
- enforce exact modules, root exports, features, dependencies, and traits
- add a clean Nix identity conformance lane for all supported variants
- verify strict host, no-std WASM, serde, docs, and doctest gates

Diffstat:
Mbuild/nix/checks.nix | 28++++++++++++++++++++++++++++
Dcontracts/conformance/vectors/identity/.gitkeep | 1-
Acontracts/conformance/vectors/identity/public_values.v1.json | 166+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/identity/src/key.rs | 7+++----
Mcrates/identity/src/lib.rs | 8+++++++-
Acrates/identity/tests/account_values.rs | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acrates/identity/tests/conformance.rs | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/identity/tests/package_boundary.rs | 157+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
8 files changed, 502 insertions(+), 9 deletions(-)

diff --git a/build/nix/checks.nix b/build/nix/checks.nix @@ -47,6 +47,33 @@ let installPhaseCommand = "mkdir -p $out"; } ); + identityConformance = common.craneLib.mkCargoDerivation ( + common.commonCraneArgs + // { + inherit (common) cargoArtifacts; + pname = "radroots-identity-conformance"; + doCheck = false; + buildPhaseCargoCommand = '' + cargo check -p radroots_identity --all-targets --no-default-features --locked + cargo check -p radroots_identity --all-targets --no-default-features --features std --locked + cargo check -p radroots_identity --all-targets --no-default-features --features serde --locked + cargo check -p radroots_identity --all-targets --locked + cargo check -p radroots_identity --all-targets --all-features --locked + cargo clippy -p radroots_identity --all-targets --no-default-features --locked -- -D warnings + cargo clippy -p radroots_identity --all-targets --no-default-features --features serde --locked -- -D warnings + cargo clippy -p radroots_identity --all-targets --all-features --locked -- -D warnings + cargo test -p radroots_identity --all-targets --no-default-features --locked + cargo test -p radroots_identity --all-targets --no-default-features --features std --locked + cargo test -p radroots_identity --all-targets --no-default-features --features serde --locked + cargo test -p radroots_identity --all-targets --all-features --locked + cargo check -p radroots_identity --no-default-features --target wasm32-unknown-unknown --locked + cargo check -p radroots_identity --no-default-features --features serde --target wasm32-unknown-unknown --locked + RUSTDOCFLAGS="-D warnings" cargo doc -p radroots_identity --all-features --no-deps --locked + cargo test -p radroots_identity --all-features --doc --locked + ''; + installPhaseCommand = "mkdir -p $out"; + } + ); mkReplicaSyncLane = { pname, @@ -84,6 +111,7 @@ in cargo-check = cargoCheck; cargo-test = cargoTest; core-conformance = coreConformance; + identity-conformance = identityConformance; replica-sync-default-check = replicaSyncDefaultCheck; replica-sync-default-test = replicaSyncDefaultTest; replica-sync-legacy-ingest-check = replicaSyncLegacyCheck; diff --git a/contracts/conformance/vectors/identity/.gitkeep b/contracts/conformance/vectors/identity/.gitkeep @@ -1 +0,0 @@ - diff --git a/contracts/conformance/vectors/identity/public_values.v1.json b/contracts/conformance/vectors/identity/public_values.v1.json @@ -0,0 +1,166 @@ +{ + "suite": "identity_public_values", + "contract_version": "1.0.0", + "vectors": [ + { + "id": "identity_public_key_canonical_hex_001", + "kind": "identity.public_key.serde", + "input": "585591529DA0BAB31B3B1B1F986611CF5F435DCA84F978C89EE8A40CCA7103DF", + "expected": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df" + }, + { + "id": "identity_identity_id_canonical_hex_002", + "kind": "identity.identity_id.serde", + "input": "E0266E3CFB0D2886F91C73F5F868F3B98273713E5FCD97C081663F5518A4B3AF", + "expected": "e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af" + }, + { + "id": "identity_account_id_canonical_hex_003", + "kind": "identity.account_id.serde", + "input": "585591529DA0BAB31B3B1B1F986611CF5F435DCA84F978C89EE8A40CCA7103DF", + "expected": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df" + }, + { + "id": "identity_username_canonical_text_004", + "kind": "identity.username.serde", + "input": " Alice.Farm ", + "expected": "alice.farm" + }, + { + "id": "identity_profile_canonical_username_005", + "kind": "identity.profile.serde", + "input": { + "username": "Alice.Farm" + }, + "expected": { + "username": "alice.farm" + } + }, + { + "id": "identity_public_identity_discards_empty_profile_006", + "kind": "identity.public_identity.serde", + "input": { + "id": "585591529DA0BAB31B3B1B1F986611CF5F435DCA84F978C89EE8A40CCA7103DF", + "public_key": "585591529DA0BAB31B3B1B1F986611CF5F435DCA84F978C89EE8A40CCA7103DF", + "profile": {} + }, + "expected": { + "id": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df", + "public_key": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df" + } + }, + { + "id": "identity_account_record_canonical_nested_values_007", + "kind": "identity.account_record.serde", + "input": { + "account_id": "585591529DA0BAB31B3B1B1F986611CF5F435DCA84F978C89EE8A40CCA7103DF", + "public_identity": { + "id": "585591529DA0BAB31B3B1B1F986611CF5F435DCA84F978C89EE8A40CCA7103DF", + "public_key": "585591529DA0BAB31B3B1B1F986611CF5F435DCA84F978C89EE8A40CCA7103DF" + }, + "label": "primary", + "created_at_unix": 10, + "updated_at_unix": 12 + }, + "expected": { + "account_id": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df", + "public_identity": { + "id": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df", + "public_key": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df" + }, + "label": "primary", + "created_at_unix": 10, + "updated_at_unix": 12 + } + }, + { + "id": "identity_account_status_canonical_ready_008", + "kind": "identity.account_status.serde", + "input": { + "ready": { + "account": { + "account_id": "585591529DA0BAB31B3B1B1F986611CF5F435DCA84F978C89EE8A40CCA7103DF", + "public_identity": { + "id": "585591529DA0BAB31B3B1B1F986611CF5F435DCA84F978C89EE8A40CCA7103DF", + "public_key": "585591529DA0BAB31B3B1B1F986611CF5F435DCA84F978C89EE8A40CCA7103DF" + }, + "created_at_unix": 10, + "updated_at_unix": 10 + } + } + }, + "expected": { + "ready": { + "account": { + "account_id": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df", + "public_identity": { + "id": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df", + "public_key": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df" + }, + "created_at_unix": 10, + "updated_at_unix": 10 + } + } + } + }, + { + "id": "identity_public_key_rejects_invalid_curve_point_009", + "kind": "identity.public_key.invalid", + "input": "0000000000000000000000000000000000000000000000000000000000000000", + "expected_error_contains": "valid secp256k1" + }, + { + "id": "identity_public_identity_rejects_mismatched_id_010", + "kind": "identity.public_identity.invalid", + "input": { + "id": "e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af", + "public_key": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df" + }, + "expected_error_contains": "does not match its public key" + }, + { + "id": "identity_account_record_rejects_mismatched_id_011", + "kind": "identity.account_record.invalid", + "input": { + "account_id": "e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af", + "public_identity": { + "id": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df", + "public_key": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df" + }, + "created_at_unix": 10, + "updated_at_unix": 10 + }, + "expected_error_contains": "does not match its public identity" + }, + { + "id": "identity_account_record_rejects_time_reversal_012", + "kind": "identity.account_record.invalid", + "input": { + "account_id": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df", + "public_identity": { + "id": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df", + "public_key": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df" + }, + "created_at_unix": 10, + "updated_at_unix": 9 + }, + "expected_error_contains": "precedes created timestamp" + }, + { + "id": "identity_username_rejects_dot_placement_013", + "kind": "identity.username.invalid", + "input": ".alice", + "expected_error_contains": "dots cannot be" + }, + { + "id": "identity_public_identity_rejects_unknown_field_014", + "kind": "identity.public_identity.invalid", + "input": { + "id": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df", + "public_key": "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df", + "secret_key": "forbidden" + }, + "expected_error_contains": "unknown field" + } + ] +} diff --git a/crates/identity/src/key.rs b/crates/identity/src/key.rs @@ -276,10 +276,9 @@ impl From<PublicKey> for IdentityId { #[cfg(test)] mod tests { - use alloc::{ - format, - string::{String, ToString}, - }; + #[cfg(feature = "serde")] + use alloc::format; + use alloc::string::{String, ToString}; use core::str::FromStr; use super::*; diff --git a/crates/identity/src/lib.rs b/crates/identity/src/lib.rs @@ -8,11 +8,17 @@ //! ```compile_fail //! use radroots_identity::{storage, IdentityError}; //! ``` +//! +//! Secret-bearing and upstream Nostr event APIs are intentionally absent: +//! +//! ```compile_fail +//! use radroots_identity::{NostrEvent, SecretKey}; +//! ``` extern crate alloc; pub mod account; -pub mod error; +mod error; pub mod key; pub mod profile; pub mod username; diff --git a/crates/identity/tests/account_values.rs b/crates/identity/tests/account_values.rs @@ -0,0 +1,61 @@ +use radroots_identity::{ + AccountId, Error, PublicIdentity, PublicKey, + account::{Record, Status}, +}; + +const ALICE: &str = "585591529da0bab31b3b1b1f986611cf5f435dca84f978c89ee8a40cca7103df"; +const BOB: &str = "e0266e3cfb0d2886f91c73f5f868f3b98273713e5fcd97c081663f5518a4b3af"; + +fn public_identity(value: &str) -> PublicIdentity { + PublicIdentity::new(PublicKey::from_hex(value).expect("valid public key")) +} + +#[test] +fn public_account_values_preserve_identity_and_readiness() { + let identity = public_identity(ALICE); + let record = Record::new(identity.clone(), Some("primary".into()), 10); + let status = Status::Ready { + account: record.clone(), + }; + + assert_eq!(record.id(), AccountId::from_public_identity(&identity)); + assert_eq!(record.public_identity(), &identity); + assert_eq!(record.label(), Some("primary")); + assert_eq!(status.account(), Some(&record)); + assert!(status.is_ready()); +} + +#[test] +fn decoded_account_parts_cannot_change_identity_or_reverse_time() { + let identity = public_identity(ALICE); + let wrong_id = AccountId::from_public_identity(&public_identity(BOB)); + + assert!(matches!( + Record::try_from_parts(wrong_id, identity.clone(), None, 10, 10), + Err(Error::AccountIdMismatch) + )); + assert!(matches!( + Record::try_from_parts( + AccountId::from_public_identity(&identity), + identity, + None, + 10, + 9, + ), + Err(Error::AccountUpdatedBeforeCreated { + created_at_unix: 10, + updated_at_unix: 9, + }) + )); +} + +#[test] +fn public_identity_values_are_send_sync_without_host_state() { + fn assert_send_sync<T: Send + Sync>() {} + + assert_send_sync::<AccountId>(); + assert_send_sync::<PublicIdentity>(); + assert_send_sync::<PublicKey>(); + assert_send_sync::<Record>(); + assert_send_sync::<Status>(); +} diff --git a/crates/identity/tests/conformance.rs b/crates/identity/tests/conformance.rs @@ -0,0 +1,83 @@ +#![cfg(feature = "serde")] + +use std::collections::BTreeSet; + +use radroots_identity::{ + AccountId, IdentityId, Profile, PublicIdentity, PublicKey, Username, + account::{Record, Status}, +}; +use serde_json::Value; + +const PUBLIC_VALUE_VECTORS: &str = + include_str!("../../../contracts/conformance/vectors/identity/public_values.v1.json"); + +#[test] +fn public_value_vectors_are_unique_complete_and_executable() { + let suite: Value = serde_json::from_str(PUBLIC_VALUE_VECTORS).expect("valid vector suite"); + assert_eq!(suite["suite"], "identity_public_values"); + assert_eq!(suite["contract_version"], "1.0.0"); + + let vectors = suite["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 vector id {id}"); + let kind = vector["kind"].as_str().expect("vector kind"); + let input = vector["input"].clone(); + if let Some(expected) = vector.get("expected") { + let actual = execute_valid(kind, input); + assert_eq!(&actual, expected, "vector {id}"); + } else { + let expected = vector["expected_error_contains"] + .as_str() + .expect("invalid vector error fragment"); + let error = execute_invalid(kind, input); + assert!( + error.contains(expected), + "vector {id} expected error containing {expected:?}, got {error:?}" + ); + } + } + assert_eq!(ids.len(), 14); +} + +fn execute_valid(kind: &str, input: Value) -> Value { + match kind { + "identity.public_key.serde" => round_trip::<PublicKey>(input), + "identity.identity_id.serde" => round_trip::<IdentityId>(input), + "identity.account_id.serde" => round_trip::<AccountId>(input), + "identity.username.serde" => round_trip::<Username>(input), + "identity.profile.serde" => round_trip::<Profile>(input), + "identity.public_identity.serde" => round_trip::<PublicIdentity>(input), + "identity.account_record.serde" => round_trip::<Record>(input), + "identity.account_status.serde" => round_trip::<Status>(input), + unsupported => panic!("unsupported valid identity vector kind {unsupported}"), + } +} + +fn execute_invalid(kind: &str, input: Value) -> String { + match kind { + "identity.public_key.invalid" => decode_error::<PublicKey>(input), + "identity.public_identity.invalid" => decode_error::<PublicIdentity>(input), + "identity.account_record.invalid" => decode_error::<Record>(input), + "identity.username.invalid" => decode_error::<Username>(input), + unsupported => panic!("unsupported invalid identity vector kind {unsupported}"), + } +} + +fn round_trip<T>(input: Value) -> Value +where + T: serde::de::DeserializeOwned + serde::Serialize, +{ + serde_json::to_value(serde_json::from_value::<T>(input).expect("valid vector input")) + .expect("serialize canonical value") +} + +fn decode_error<T>(input: Value) -> String +where + T: serde::de::DeserializeOwned + core::fmt::Debug, +{ + serde_json::from_value::<T>(input) + .expect_err("invalid vector must be rejected") + .to_string() +} diff --git a/crates/identity/tests/package_boundary.rs b/crates/identity/tests/package_boundary.rs @@ -1,9 +1,39 @@ +use std::{collections::BTreeSet, fs, path::Path}; + +use radroots_identity::{ + AccountId, Error, IdentityId, Profile, PublicIdentity, PublicKey, Username, + account::{Record, Status}, +}; + const MANIFEST: &str = include_str!("../Cargo.toml"); +const ROOT: &str = include_str!("../src/lib.rs"); #[test] -fn manifest_has_no_host_persistence_feature_or_dependency() { +fn manifest_has_exact_features_and_dependencies() { + assert_eq!( + table_keys(MANIFEST, "[features]"), + BTreeSet::from(["default", "serde", "std"]) + ); + assert_eq!( + table_keys(MANIFEST, "[dependencies]"), + BTreeSet::from(["k256", "serde", "thiserror"]) + ); + assert_eq!( + table_keys(MANIFEST, "[dev-dependencies]"), + BTreeSet::from(["serde_json"]) + ); + assert!(MANIFEST.contains("default = [\"std\", \"serde\"]")); + assert!(MANIFEST.contains("std = [\"thiserror/std\"]")); + assert!(MANIFEST.contains("serde = [\"dep:serde\"]")); + assert!(MANIFEST.contains( + "k256 = { version = \"0.13\", default-features = false, features = [\"arithmetic\"] }" + )); + assert!(MANIFEST.contains("serde = { workspace = true, optional = true }")); + assert!(MANIFEST.contains("thiserror = { version = \"2\", default-features = false }")); + for forbidden in [ "json-file", + "nostr", "radroots_protected_store", "radroots_runtime", "radroots_runtime_paths", @@ -16,7 +46,128 @@ fn manifest_has_no_host_persistence_feature_or_dependency() { "identity manifest must not contain host persistence edge {forbidden}" ); } +} - assert!(MANIFEST.contains("default = [\"std\", \"serde\"]")); - assert!(MANIFEST.contains("std = [\"thiserror/std\"]")); +#[test] +fn crate_root_matches_the_curated_module_and_export_contract() { + assert!(ROOT.contains("#![cfg_attr(not(feature = \"std\"), no_std)]")); + assert_eq!( + root_declarations("pub mod "), + BTreeSet::from(["account", "key", "profile", "username"]) + ); + assert_eq!(root_declarations("mod "), BTreeSet::from(["error"])); + assert_eq!( + ROOT.lines() + .map(str::trim) + .filter(|line| line.starts_with("pub use ")) + .collect::<BTreeSet<_>>(), + BTreeSet::from([ + "pub use account::AccountId;", + "pub use error::Error;", + "pub use key::{IdentityId, PublicKey};", + "pub use profile::{Profile, PublicIdentity};", + "pub use username::Username;", + ]) + ); +} + +#[test] +fn intended_public_paths_and_traits_compile() { + fn assert_public_value<T: Clone + core::fmt::Debug + Eq + Send + Sync>() {} + + assert_public_value::<AccountId>(); + assert_public_value::<IdentityId>(); + assert_public_value::<Profile>(); + assert_public_value::<PublicIdentity>(); + assert_public_value::<PublicKey>(); + assert_public_value::<Record>(); + assert_public_value::<Status>(); + assert_public_value::<Username>(); + let _ = core::mem::size_of::<Error>(); +} + +#[test] +fn production_sources_have_no_forbidden_owners_or_public_traits() { + let source_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); + let mut sources = Vec::new(); + collect_rust_sources(&source_root, &mut sources); + assert!(!sources.is_empty()); + + for path in sources { + let source = fs::read_to_string(&path).expect("read identity source"); + let production = source.split("\n#[cfg(test)]").next().unwrap_or(&source); + for line in production.lines() { + let trimmed = line.trim_start(); + if trimmed.starts_with("//") { + continue; + } + assert!( + !trimmed.starts_with("pub trait ") && !trimmed.starts_with("pub unsafe trait "), + "identity must not publish traits: {}: {trimmed}", + path.display() + ); + for forbidden in [ + "RadrootsIdentity", + "RadrootsSecret", + "SecretKey", + "PrivateKey", + "secret_key", + "Nsec", + "nsec::", + "nip49", + "Nip49", + "nostr::", + "std::fs", + "std::path", + "PathBuf", + "Sqlite", + "keyring", + ] { + assert!( + !line.contains(forbidden), + "identity production source must not contain {forbidden}: {}: {trimmed}", + path.display() + ); + } + } + } +} + +fn table_keys<'a>(manifest: &'a str, heading: &str) -> BTreeSet<&'a str> { + let table = manifest + .split_once(heading) + .unwrap_or_else(|| panic!("missing manifest table {heading}")) + .1; + table + .lines() + .skip(1) + .take_while(|line| !line.trim_start().starts_with('[')) + .filter_map(|line| { + let line = line.trim(); + (!line.is_empty() && !line.starts_with('#')) + .then(|| line.split_once('=').map(|(key, _)| key.trim())) + .flatten() + }) + .collect() +} + +fn root_declarations(prefix: &str) -> BTreeSet<&str> { + ROOT.lines() + .map(str::trim) + .filter_map(|line| { + line.strip_prefix(prefix) + .and_then(|name| name.strip_suffix(';')) + }) + .collect() +} + +fn collect_rust_sources(directory: &Path, paths: &mut Vec<std::path::PathBuf>) { + for entry in fs::read_dir(directory).expect("read identity source directory") { + let path = entry.expect("source directory entry").path(); + if path.is_dir() { + collect_rust_sources(&path, paths); + } else if path.extension().and_then(|value| value.to_str()) == Some("rs") { + paths.push(path); + } + } }