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:
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()
+ }
+ );
+ }
+}