lib

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

commit c3508928e31e68ea720a9f470b3fb1ee0b21d313
parent f9a96db8b74ca4be5d7b5c429991b21823944793
Author: triesap <tyson@radroots.org>
Date:   Sun, 14 Jun 2026 00:55:20 -0700

authority: define frozen draft signer trait

- add the RadrootsEventSigner abstraction for frozen drafts
- introduce typed signer errors under the authority error surface
- cover signer pubkey reporting and deterministic error propagation
- validate with cargo fmt, check, and tests for radroots_authority

Diffstat:
Mcrates/authority/src/error.rs | 20++++++++++++++++++++
Mcrates/authority/src/lib.rs | 4++--
Mcrates/authority/src/signer.rs | 137++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
3 files changed, 158 insertions(+), 3 deletions(-)

diff --git a/crates/authority/src/error.rs b/crates/authority/src/error.rs @@ -2,6 +2,11 @@ use thiserror::Error; +#[cfg(not(feature = "std"))] +use alloc::string::String; +#[cfg(feature = "std")] +use std::string::String; + #[derive(Debug, Error, PartialEq, Eq)] pub enum RadrootsAuthorityError { #[error("invalid actor public key")] @@ -9,4 +14,19 @@ pub enum RadrootsAuthorityError { #[error("invalid signer public key")] InvalidSignerPubkey, + + #[error("signer error: {0}")] + Signer(#[from] RadrootsSignerError), +} + +#[derive(Debug, Error, PartialEq, Eq)] +pub enum RadrootsSignerError { + #[error("signer unavailable")] + Unavailable, + + #[error("signer rejected draft")] + Rejected, + + #[error("signing failed: {message}")] + SigningFailed { message: String }, } diff --git a/crates/authority/src/lib.rs b/crates/authority/src/lib.rs @@ -13,5 +13,5 @@ pub use actor::{ RadrootsActorContext, RadrootsActorResolutionRequest, RadrootsActorSelector, RadrootsActorSource, role_satisfies, }; -pub use error::RadrootsAuthorityError; -pub use signer::RadrootsSignerIdentity; +pub use error::{RadrootsAuthorityError, RadrootsSignerError}; +pub use signer::{RadrootsEventSigner, RadrootsSignerIdentity}; diff --git a/crates/authority/src/signer.rs b/crates/authority/src/signer.rs @@ -1,6 +1,7 @@ #![forbid(unsafe_code)] -use crate::RadrootsAuthorityError; +use crate::{RadrootsAuthorityError, RadrootsSignerError}; +use radroots_events::draft::{RadrootsFrozenEventDraft, RadrootsSignedNostrEvent}; use radroots_events::ids::RadrootsPublicKey; #[derive(Clone, Debug, PartialEq, Eq)] @@ -19,3 +20,137 @@ impl RadrootsSignerIdentity { &self.pubkey } } + +pub trait RadrootsEventSigner { + fn pubkey(&self) -> &RadrootsPublicKey; + + fn sign_frozen_draft( + &self, + draft: &RadrootsFrozenEventDraft, + ) -> Result<RadrootsSignedNostrEvent, RadrootsSignerError>; +} + +#[cfg(test)] +mod tests { + use super::*; + use radroots_events::kinds::KIND_POST; + + fn hex_64(character: char) -> String { + std::iter::repeat_n(character, 64).collect() + } + + fn hex_128(character: char) -> String { + std::iter::repeat_n(character, 128).collect() + } + + fn draft_for(pubkey: &str) -> RadrootsFrozenEventDraft { + RadrootsFrozenEventDraft::new( + "radroots.social.post.v1", + KIND_POST, + 1_700_000_000, + vec![vec!["t".to_owned(), "soil".to_owned()]], + "hello", + pubkey, + ) + .expect("draft") + } + + struct MockSigner { + pubkey: RadrootsPublicKey, + failure: Option<RadrootsSignerError>, + } + + impl MockSigner { + fn new(pubkey: &str) -> Self { + Self { + pubkey: RadrootsPublicKey::parse(pubkey).expect("pubkey"), + failure: None, + } + } + + fn failing(pubkey: &str, failure: RadrootsSignerError) -> Self { + Self { + pubkey: RadrootsPublicKey::parse(pubkey).expect("pubkey"), + failure: Some(failure), + } + } + } + + impl RadrootsEventSigner for MockSigner { + fn pubkey(&self) -> &RadrootsPublicKey { + &self.pubkey + } + + fn sign_frozen_draft( + &self, + draft: &RadrootsFrozenEventDraft, + ) -> Result<RadrootsSignedNostrEvent, RadrootsSignerError> { + if let Some(failure) = &self.failure { + return Err(match failure { + RadrootsSignerError::Unavailable => RadrootsSignerError::Unavailable, + RadrootsSignerError::Rejected => RadrootsSignerError::Rejected, + RadrootsSignerError::SigningFailed { message } => { + RadrootsSignerError::SigningFailed { + message: message.clone(), + } + } + }); + } + RadrootsSignedNostrEvent::new( + draft.expected_event_id.as_str(), + self.pubkey.as_str(), + draft.created_at, + draft.kind, + draft.tags.clone(), + draft.content.as_str(), + hex_128('f'), + "{}", + ) + .map_err(|error| RadrootsSignerError::SigningFailed { + message: error.to_string(), + }) + } + } + + #[test] + fn mock_signer_reports_public_key() { + let pubkey = hex_64('a'); + let signer = MockSigner::new(pubkey.as_str()); + + assert_eq!(signer.pubkey().as_str(), pubkey); + } + + #[test] + fn mock_signer_returns_signed_frozen_draft() { + let pubkey = hex_64('a'); + let signer = MockSigner::new(pubkey.as_str()); + let draft = draft_for(pubkey.as_str()); + + let signed = signer.sign_frozen_draft(&draft).expect("signed"); + + assert_eq!(signed.id, draft.expected_event_id); + assert_eq!(signed.pubkey, pubkey); + assert_eq!(signed.kind, KIND_POST); + } + + #[test] + fn mock_signer_propagates_signing_errors() { + let pubkey = hex_64('a'); + let signer = MockSigner::failing( + pubkey.as_str(), + RadrootsSignerError::SigningFailed { + message: "deterministic failure".to_owned(), + }, + ); + let draft = draft_for(pubkey.as_str()); + + let err = signer.sign_frozen_draft(&draft).expect_err("failure"); + + assert_eq!( + err, + RadrootsSignerError::SigningFailed { + message: "deterministic failure".to_owned() + } + ); + } +}