commit 8302eb065eacdf3183079ff81e3756a1a8d14b99
parent 433aa20dc5141f071545d46445e8792f86dc8d8e
Author: triesap <tyson@radroots.org>
Date: Sun, 14 Jun 2026 01:54:55 -0700
runtime: extract relay auth module
- move BaseAuthState and AUTH parsing into relay::auth
- update runtime config, relay tests, and benchmarks to import AUTH directly
- keep AUTH challenge and rejection behavior unchanged
- verify formatting, runtime tests, and benchmark compile checks stay green
Diffstat:
7 files changed, 221 insertions(+), 188 deletions(-)
diff --git a/crates/tangle_bench/src/lib.rs b/crates/tangle_bench/src/lib.rs
@@ -11,7 +11,7 @@ use tangle_groups::{KIND_GROUP_ADMINS, KIND_GROUP_MEMBERS, KIND_GROUP_METADATA,
use tangle_protocol::{
Event, Filter, RelayMessage, SubscriptionId, event_to_value, filter_from_value,
};
-use tangle_runtime::base_relay::{BaseAuthState, BaseRelay};
+use tangle_runtime::{base_relay::BaseRelay, relay::auth::BaseAuthState};
use tangle_store_pocket::{PocketStoreConfig, PocketSyncPolicy};
use tangle_test_support::{
FixtureKey, TANGLE_V2_RELAY_URL, tangle_v2_auth_event, tangle_v2_event, tangle_v2_group_config,
diff --git a/crates/tangle_runtime/src/base_relay.rs b/crates/tangle_runtime/src/base_relay.rs
@@ -1,5 +1,6 @@
use crate::errors::{BaseRelayError, ok_accepted, ok_rejected};
use crate::ops::BaseRelayReadinessState;
+use crate::relay::auth::BaseAuthState;
use std::{collections::BTreeMap, collections::BTreeSet, str};
use tangle_crypto::{RelaySigner, verify_event_signature};
use tangle_groups::{
@@ -24,154 +25,6 @@ use tangle_store_pocket::{
TANGLE_GROUP_PROJECTION_TABLE, parse_pocket_event_json, parse_pocket_filter_json,
};
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct BaseAuthState {
- relay_url: String,
- ttl_seconds: u64,
- challenge: Option<BaseAuthChallenge>,
- authenticated_pubkeys: BTreeSet<PublicKeyHex>,
-}
-
-impl BaseAuthState {
- pub fn new(relay_url: impl Into<String>, ttl_seconds: u64) -> Result<Self, BaseRelayError> {
- let relay_url = relay_url.into();
- if relay_url.trim().is_empty() {
- return Err(BaseRelayError::invalid("auth relay URL must not be empty"));
- }
- if ttl_seconds == 0 {
- return Err(BaseRelayError::invalid(
- "auth challenge ttl must be greater than zero",
- ));
- }
- Ok(Self {
- relay_url,
- ttl_seconds,
- challenge: None,
- authenticated_pubkeys: BTreeSet::new(),
- })
- }
-
- pub fn issue_challenge(
- &mut self,
- challenge: impl Into<String>,
- issued_at: UnixTimestamp,
- ) -> Result<RelayMessage, BaseRelayError> {
- let challenge = challenge.into();
- if challenge.is_empty() {
- return Err(BaseRelayError::invalid("auth challenge must not be empty"));
- }
- self.challenge = Some(BaseAuthChallenge {
- value: challenge.clone(),
- issued_at,
- });
- Ok(RelayMessage::Auth(challenge))
- }
-
- pub fn authenticate(
- &mut self,
- event: &Event,
- now: UnixTimestamp,
- ) -> Result<PublicKeyHex, BaseRelayError> {
- verify_event_signature(event).map_err(BaseRelayError::invalid)?;
- let auth = parse_base_relay_auth_event(event)
- .map_err(BaseRelayError::invalid)?
- .ok_or_else(|| BaseRelayError::invalid("AUTH message must contain kind 22242"))?;
- let challenge = self
- .challenge
- .as_ref()
- .ok_or_else(|| BaseRelayError::auth_required("auth challenge is missing"))?;
- if auth.relay() != self.relay_url {
- return Err(BaseRelayError::auth_required(
- "auth relay does not match canonical relay URL",
- ));
- }
- if auth.challenge() != challenge.value {
- return Err(BaseRelayError::auth_required(
- "auth challenge does not match",
- ));
- }
- if now.as_u64()
- > challenge
- .issued_at
- .as_u64()
- .saturating_add(self.ttl_seconds)
- {
- return Err(BaseRelayError::auth_required("auth challenge expired"));
- }
- let pubkey = auth.pubkey().clone();
- self.authenticated_pubkeys.insert(pubkey.clone());
- Ok(pubkey)
- }
-
- pub fn authenticated_pubkeys(&self) -> &BTreeSet<PublicKeyHex> {
- &self.authenticated_pubkeys
- }
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-struct BaseAuthChallenge {
- value: String,
- issued_at: UnixTimestamp,
-}
-
-#[derive(Debug, Clone, PartialEq, Eq)]
-struct BaseRelayAuthEvent {
- pubkey: PublicKeyHex,
- relay: String,
- challenge: String,
-}
-
-impl BaseRelayAuthEvent {
- fn pubkey(&self) -> &PublicKeyHex {
- &self.pubkey
- }
-
- fn relay(&self) -> &str {
- &self.relay
- }
-
- fn challenge(&self) -> &str {
- &self.challenge
- }
-}
-
-fn parse_base_relay_auth_event(event: &Event) -> Result<Option<BaseRelayAuthEvent>, String> {
- if event.unsigned().kind().as_u32() != 22_242 {
- return Ok(None);
- }
- let relay = required_single_tag_value(event, "relay")?;
- let challenge = required_single_tag_value(event, "challenge")?;
- if relay.is_empty() {
- return Err("relay auth relay tag must not be empty".to_owned());
- }
- if challenge.is_empty() {
- return Err("relay auth challenge tag must not be empty".to_owned());
- }
- Ok(Some(BaseRelayAuthEvent {
- pubkey: event.unsigned().pubkey().clone(),
- relay,
- challenge,
- }))
-}
-
-fn required_single_tag_value(event: &Event, name: &str) -> Result<String, String> {
- let mut matches = event
- .unsigned()
- .tags()
- .iter()
- .filter(|tag| tag.name().as_str() == name);
- let tag = matches
- .next()
- .ok_or_else(|| format!("tag `{name}` is required"))?;
- if matches.next().is_some() {
- return Err(format!("tag `{name}` must not be repeated"));
- }
- tag.values()
- .get(1)
- .cloned()
- .ok_or_else(|| format!("tag `{name}` must include a value"))
-}
-
pub struct BaseRelay {
store: PocketStoreHandle,
subscriptions: LiveSubscriptionSet,
@@ -1113,7 +966,8 @@ fn pocket_event_id(event_id: &EventId) -> Result<PocketEventId, BaseRelayError>
#[cfg(test)]
mod tests {
- use super::{BaseAuthState, BaseRelay, CloseResult};
+ use super::{BaseRelay, CloseResult};
+ use crate::relay::auth::BaseAuthState;
use tangle_crypto::RelaySigner;
use tangle_groups::{
GroupId, KIND_GROUP_ADMINS, KIND_GROUP_CREATE_GROUP, KIND_GROUP_CREATE_INVITE,
@@ -1127,39 +981,6 @@ mod tests {
};
use tangle_store_pocket::{PocketStoreConfig, PocketSyncPolicy};
#[test]
- fn auth_state_issues_challenges_and_accepts_multiple_pubkeys() {
- let mut auth = BaseAuthState::new("wss://relay.radroots.test", 60).expect("auth state");
- let issued = UnixTimestamp::new(100);
-
- assert_eq!(
- auth.issue_challenge("challenge-a", issued)
- .expect("challenge"),
- RelayMessage::Auth("challenge-a".to_owned())
- );
-
- let first = signed_auth_event(7, "challenge-a", 120);
- let second = signed_auth_event(8, "challenge-a", 130);
-
- let first_pubkey = auth
- .authenticate(&first, UnixTimestamp::new(120))
- .expect("first");
- let second_pubkey = auth
- .authenticate(&second, UnixTimestamp::new(130))
- .expect("second");
-
- assert_ne!(first_pubkey, second_pubkey);
- assert!(auth.authenticated_pubkeys().contains(&first_pubkey));
- assert!(auth.authenticated_pubkeys().contains(&second_pubkey));
- assert_eq!(auth.authenticated_pubkeys().len(), 2);
- assert_eq!(
- auth.authenticate(&signed_auth_event(9, "wrong", 130), UnixTimestamp::new(130))
- .expect_err("wrong")
- .prefixed_message(),
- "auth-required: auth challenge does not match"
- );
- }
-
- #[test]
fn base_relay_stores_queries_counts_closes_and_fans_out_public_events() {
let mut relay = test_relay("base-relay-public", 4);
let event = signed_public_event(7, 1, Vec::new(), "hello");
diff --git a/crates/tangle_runtime/src/config.rs b/crates/tangle_runtime/src/config.rs
@@ -1,9 +1,6 @@
#![forbid(unsafe_code)]
-use crate::{
- base_relay::{BaseAuthState, BaseRelay},
- errors::BaseRelayError,
-};
+use crate::{base_relay::BaseRelay, errors::BaseRelayError, relay::auth::BaseAuthState};
use serde::Deserialize;
use std::{net::SocketAddr, path::PathBuf};
use tangle_groups::GroupRuntimeConfig;
diff --git a/crates/tangle_runtime/src/lib.rs b/crates/tangle_runtime/src/lib.rs
@@ -6,6 +6,7 @@ pub mod config;
pub mod errors;
pub mod nip11;
pub mod ops;
+pub mod relay;
use std::{fmt, fs, path::Path, path::PathBuf};
diff --git a/crates/tangle_runtime/src/relay/auth.rs b/crates/tangle_runtime/src/relay/auth.rs
@@ -0,0 +1,210 @@
+#![forbid(unsafe_code)]
+
+use crate::errors::BaseRelayError;
+use std::collections::BTreeSet;
+use tangle_crypto::verify_event_signature;
+use tangle_protocol::{Event, PublicKeyHex, RelayMessage, UnixTimestamp};
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct BaseAuthState {
+ relay_url: String,
+ ttl_seconds: u64,
+ challenge: Option<BaseAuthChallenge>,
+ authenticated_pubkeys: BTreeSet<PublicKeyHex>,
+}
+
+impl BaseAuthState {
+ pub fn new(relay_url: impl Into<String>, ttl_seconds: u64) -> Result<Self, BaseRelayError> {
+ let relay_url = relay_url.into();
+ if relay_url.trim().is_empty() {
+ return Err(BaseRelayError::invalid("auth relay URL must not be empty"));
+ }
+ if ttl_seconds == 0 {
+ return Err(BaseRelayError::invalid(
+ "auth challenge ttl must be greater than zero",
+ ));
+ }
+ Ok(Self {
+ relay_url,
+ ttl_seconds,
+ challenge: None,
+ authenticated_pubkeys: BTreeSet::new(),
+ })
+ }
+
+ pub fn issue_challenge(
+ &mut self,
+ challenge: impl Into<String>,
+ issued_at: UnixTimestamp,
+ ) -> Result<RelayMessage, BaseRelayError> {
+ let challenge = challenge.into();
+ if challenge.is_empty() {
+ return Err(BaseRelayError::invalid("auth challenge must not be empty"));
+ }
+ self.challenge = Some(BaseAuthChallenge {
+ value: challenge.clone(),
+ issued_at,
+ });
+ Ok(RelayMessage::Auth(challenge))
+ }
+
+ pub fn authenticate(
+ &mut self,
+ event: &Event,
+ now: UnixTimestamp,
+ ) -> Result<PublicKeyHex, BaseRelayError> {
+ verify_event_signature(event).map_err(BaseRelayError::invalid)?;
+ let auth = parse_base_relay_auth_event(event)
+ .map_err(BaseRelayError::invalid)?
+ .ok_or_else(|| BaseRelayError::invalid("AUTH message must contain kind 22242"))?;
+ let challenge = self
+ .challenge
+ .as_ref()
+ .ok_or_else(|| BaseRelayError::auth_required("auth challenge is missing"))?;
+ if auth.relay() != self.relay_url {
+ return Err(BaseRelayError::auth_required(
+ "auth relay does not match canonical relay URL",
+ ));
+ }
+ if auth.challenge() != challenge.value {
+ return Err(BaseRelayError::auth_required(
+ "auth challenge does not match",
+ ));
+ }
+ if now.as_u64()
+ > challenge
+ .issued_at
+ .as_u64()
+ .saturating_add(self.ttl_seconds)
+ {
+ return Err(BaseRelayError::auth_required("auth challenge expired"));
+ }
+ let pubkey = auth.pubkey().clone();
+ self.authenticated_pubkeys.insert(pubkey.clone());
+ Ok(pubkey)
+ }
+
+ pub fn authenticated_pubkeys(&self) -> &BTreeSet<PublicKeyHex> {
+ &self.authenticated_pubkeys
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+struct BaseAuthChallenge {
+ value: String,
+ issued_at: UnixTimestamp,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+struct BaseRelayAuthEvent {
+ pubkey: PublicKeyHex,
+ relay: String,
+ challenge: String,
+}
+
+impl BaseRelayAuthEvent {
+ fn pubkey(&self) -> &PublicKeyHex {
+ &self.pubkey
+ }
+
+ fn relay(&self) -> &str {
+ &self.relay
+ }
+
+ fn challenge(&self) -> &str {
+ &self.challenge
+ }
+}
+
+fn parse_base_relay_auth_event(event: &Event) -> Result<Option<BaseRelayAuthEvent>, String> {
+ if event.unsigned().kind().as_u32() != 22_242 {
+ return Ok(None);
+ }
+ let relay = required_single_tag_value(event, "relay")?;
+ let challenge = required_single_tag_value(event, "challenge")?;
+ if relay.is_empty() {
+ return Err("relay auth relay tag must not be empty".to_owned());
+ }
+ if challenge.is_empty() {
+ return Err("relay auth challenge tag must not be empty".to_owned());
+ }
+ Ok(Some(BaseRelayAuthEvent {
+ pubkey: event.unsigned().pubkey().clone(),
+ relay,
+ challenge,
+ }))
+}
+
+fn required_single_tag_value(event: &Event, name: &str) -> Result<String, String> {
+ let mut matches = event
+ .unsigned()
+ .tags()
+ .iter()
+ .filter(|tag| tag.name().as_str() == name);
+ let tag = matches
+ .next()
+ .ok_or_else(|| format!("tag `{name}` is required"))?;
+ if matches.next().is_some() {
+ return Err(format!("tag `{name}` must not be repeated"));
+ }
+ tag.values()
+ .get(1)
+ .cloned()
+ .ok_or_else(|| format!("tag `{name}` must include a value"))
+}
+
+#[cfg(test)]
+mod tests {
+ use super::BaseAuthState;
+ use tangle_crypto::RelaySigner;
+ use tangle_protocol::{Event, Kind, RelayMessage, Tag, UnixTimestamp, UnsignedEvent};
+
+ #[test]
+ fn auth_state_issues_challenges_and_accepts_multiple_pubkeys() {
+ let mut auth = BaseAuthState::new("wss://relay.radroots.test", 60).expect("auth state");
+ let issued = UnixTimestamp::new(100);
+
+ assert_eq!(
+ auth.issue_challenge("challenge-a", issued)
+ .expect("challenge"),
+ RelayMessage::Auth("challenge-a".to_owned())
+ );
+
+ let first = signed_auth_event(7, "challenge-a", 120);
+ let second = signed_auth_event(8, "challenge-a", 130);
+
+ let first_pubkey = auth
+ .authenticate(&first, UnixTimestamp::new(120))
+ .expect("first");
+ let second_pubkey = auth
+ .authenticate(&second, UnixTimestamp::new(130))
+ .expect("second");
+
+ assert_ne!(first_pubkey, second_pubkey);
+ assert!(auth.authenticated_pubkeys().contains(&first_pubkey));
+ assert!(auth.authenticated_pubkeys().contains(&second_pubkey));
+ assert_eq!(auth.authenticated_pubkeys().len(), 2);
+ assert_eq!(
+ auth.authenticate(&signed_auth_event(9, "wrong", 130), UnixTimestamp::new(130))
+ .expect_err("wrong")
+ .prefixed_message(),
+ "auth-required: auth challenge does not match"
+ );
+ }
+
+ fn signed_auth_event(secret_byte: u8, challenge: &str, created_at: u64) -> Event {
+ let secret = format!("{:02x}", secret_byte).repeat(32);
+ let signer = RelaySigner::from_secret_hex(&secret).expect("signer");
+ let unsigned = UnsignedEvent::new(
+ signer.public_key().clone(),
+ UnixTimestamp::new(created_at),
+ Kind::new(22_242).expect("kind"),
+ vec![
+ Tag::from_parts("relay", &["wss://relay.radroots.test"]).expect("relay"),
+ Tag::from_parts("challenge", &[challenge]).expect("challenge"),
+ ],
+ "",
+ );
+ signer.sign_unsigned_event(unsigned)
+ }
+}
diff --git a/crates/tangle_runtime/src/relay/mod.rs b/crates/tangle_runtime/src/relay/mod.rs
@@ -0,0 +1,3 @@
+#![forbid(unsafe_code)]
+
+pub mod auth;
diff --git a/crates/tangle_runtime/tests/base_relay_v2.rs b/crates/tangle_runtime/tests/base_relay_v2.rs
@@ -11,8 +11,9 @@ use tangle_protocol::{
filter_from_value, parse_client_message, parse_event_json,
};
use tangle_runtime::{
- base_relay::{BaseAuthState, BaseRelay, CloseResult},
+ base_relay::{BaseRelay, CloseResult},
nip11::{BASE_RELAY_SUPPORTED_NIPS, BaseRelayInfoConfig},
+ relay::auth::BaseAuthState,
};
use tangle_store_pocket::{PocketStoreConfig, PocketSyncPolicy};
use tangle_test_support::{