commit bfaa2cc638ef740b28ab42fb246a2a792e4805ba
parent 5868a5a46979d1c7acd588a8c760b3dfde88067e
Author: triesap <tyson@radroots.org>
Date: Mon, 15 Jun 2026 18:58:57 -0700
runtime: validate auth through Pocket events
- add Pocket event identity signature and protected-tag helpers
- authenticate NIP-42 AUTH events without protocol conversion
- rate limit Pocket AUTH attempts and failures directly
- cover Pocket signature protected tag and auth edge cases
Diffstat:
7 files changed, 582 insertions(+), 42 deletions(-)
diff --git a/crates/tangle_bench/src/lib.rs b/crates/tangle_bench/src/lib.rs
@@ -9,8 +9,7 @@ use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Instant;
use tangle_groups::{KIND_GROUP_ADMINS, KIND_GROUP_MEMBERS, KIND_GROUP_METADATA, MemberStatus};
use tangle_protocol::{
- ClientMessage, Event, Filter, RelayMessage, SubscriptionId, UnixTimestamp, event_to_value,
- filter_from_value,
+ Event, Filter, RelayMessage, SubscriptionId, UnixTimestamp, event_to_value, filter_from_value,
};
use tangle_runtime::{
config::{BaseRelayRuntimeConfig, parse_base_relay_runtime_config_json},
@@ -20,7 +19,9 @@ use tangle_runtime::{
},
runtime::{TangleRuntime, TangleRuntimeHandle},
};
-use tangle_store_pocket::{PocketQueryConfig, PocketStoreConfig, PocketSyncPolicy};
+use tangle_store_pocket::{
+ PocketQueryConfig, PocketStoreConfig, PocketSyncPolicy, parse_pocket_filter_json,
+};
use tangle_test_support::{
FixtureKey, TANGLE_V2_RELAY_URL, tangle_v2_auth_event, tangle_v2_event, tangle_v2_group_config,
tangle_v2_group_create_event, tangle_v2_group_event, tangle_v2_put_user_event, tangle_v2_tag,
@@ -1408,12 +1409,12 @@ async fn runtime_count_resource_control_probe() -> Result<CountResourceControlPr
for (name, filter) in cases {
let sample = Instant::now();
let subscription_id = subscription(name)?;
+ let pocket_filter = parse_pocket_filter_json(filter.to_string().as_bytes())
+ .map_err(|error| error.to_string())?;
let replies = handle
- .handle_client_message(
- ClientMessage::Count {
- subscription_id: subscription_id.clone(),
- filters: vec![filter_from_value(&filter)?],
- },
+ .handle_count_pocket(
+ subscription_id.clone(),
+ vec![pocket_filter],
&mut auth,
UnixTimestamp::new(1_714_700_000),
)
diff --git a/crates/tangle_crypto/src/lib.rs b/crates/tangle_crypto/src/lib.rs
@@ -60,6 +60,35 @@ pub fn verify_event_signature(event: &Event) -> Result<(), String> {
.map_err(|_| "event signature verification failed".to_owned())
}
+pub fn compute_event_id_hex_from_canonical_json(canonical: &str) -> String {
+ let digest = Sha256::digest(canonical.as_bytes());
+ lower_hex(&digest)
+}
+
+pub fn verify_event_signature_bytes(
+ canonical: &str,
+ event_id: &[u8; 32],
+ pubkey: &[u8; 32],
+ signature: &[u8; 64],
+) -> Result<(), String> {
+ let expected_id = Sha256::digest(canonical.as_bytes());
+ let expected_id_bytes: &[u8] = expected_id.as_ref();
+ if expected_id_bytes != event_id {
+ return Err(format!(
+ "event id mismatch: expected {}, got {}",
+ lower_hex(&expected_id),
+ lower_hex(event_id)
+ ));
+ }
+ let verifying_key = VerifyingKey::from_bytes(pubkey)
+ .map_err(|_| "event public key is not a valid secp256k1 x-only key".to_owned())?;
+ let signature = Signature::try_from(signature.as_slice())
+ .map_err(|_| "event signature is not a valid schnorr signature".to_owned())?;
+ verifying_key
+ .verify(event_id, &signature)
+ .map_err(|_| "event signature verification failed".to_owned())
+}
+
pub struct RelaySigner {
signing_key: SigningKey,
public_key: PublicKeyHex,
diff --git a/crates/tangle_runtime/src/lib.rs b/crates/tangle_runtime/src/lib.rs
@@ -9,6 +9,7 @@ pub mod logging;
pub mod nip11;
pub mod ops;
pub(crate) mod pocket_conversion;
+pub(crate) mod pocket_event_validation;
pub mod rate_limits;
pub mod relay;
pub mod runtime;
diff --git a/crates/tangle_runtime/src/pocket_event_validation.rs b/crates/tangle_runtime/src/pocket_event_validation.rs
@@ -0,0 +1,198 @@
+#![forbid(unsafe_code)]
+
+use crate::errors::BaseRelayError;
+use std::str;
+use tangle_protocol::{EventId, Kind, PublicKeyHex, UnixTimestamp};
+use tangle_store_pocket::PocketEvent;
+
+pub(crate) fn pocket_event_id(event: &PocketEvent) -> Result<EventId, BaseRelayError> {
+ EventId::new(&event.id().as_hex_string()).map_err(BaseRelayError::error)
+}
+
+pub(crate) fn pocket_event_pubkey(event: &PocketEvent) -> Result<PublicKeyHex, BaseRelayError> {
+ PublicKeyHex::new(&event.pubkey().as_hex_string()).map_err(BaseRelayError::error)
+}
+
+pub(crate) fn pocket_event_kind(event: &PocketEvent) -> Result<Kind, BaseRelayError> {
+ Kind::new(u64::from(event.kind().as_u16())).map_err(BaseRelayError::error)
+}
+
+pub(crate) fn pocket_event_created_at(event: &PocketEvent) -> UnixTimestamp {
+ UnixTimestamp::new(event.created_at().as_u64())
+}
+
+pub(crate) fn validate_pocket_event_shape(
+ event: &PocketEvent,
+ max_event_tags: usize,
+ max_content_length: usize,
+) -> Result<(), BaseRelayError> {
+ let tags = event.tags().map_err(|error| {
+ BaseRelayError::invalid(format!("malformed Pocket event tags: {error}"))
+ })?;
+ if tags.count() > max_event_tags {
+ return Err(BaseRelayError::invalid(format!(
+ "event tag count exceeds runtime max_event_tags {max_event_tags}"
+ )));
+ }
+ if event.content().len() > max_content_length {
+ return Err(BaseRelayError::invalid(format!(
+ "event content length exceeds runtime max_content_length {max_content_length}"
+ )));
+ }
+ Ok(())
+}
+
+pub(crate) fn is_pocket_nip70_protected_event(event: &PocketEvent) -> Result<bool, BaseRelayError> {
+ let tags = event.tags().map_err(|error| {
+ BaseRelayError::invalid(format!("malformed Pocket event tags: {error}"))
+ })?;
+ for tag in tags.iter() {
+ if tag
+ .into_iter()
+ .next()
+ .map(|value| value == b"-")
+ .unwrap_or(false)
+ {
+ return Ok(true);
+ }
+ }
+ Ok(false)
+}
+
+pub(crate) fn verify_pocket_event_signature(event: &PocketEvent) -> Result<(), BaseRelayError> {
+ let canonical = pocket_canonical_event_json(event)?;
+ tangle_crypto::verify_event_signature_bytes(
+ &canonical,
+ &event.id().into_inner(),
+ event.pubkey().as_bytes(),
+ &event.sig().into_inner(),
+ )
+ .map_err(BaseRelayError::invalid)
+}
+
+pub(crate) fn pocket_canonical_event_json(event: &PocketEvent) -> Result<String, BaseRelayError> {
+ let tags = event
+ .tags()
+ .map_err(|error| BaseRelayError::invalid(format!("malformed Pocket event tags: {error}")))?
+ .iter()
+ .map(|tag| {
+ tag.map(|value| {
+ str::from_utf8(value)
+ .map(|value| serde_json::Value::String(value.to_owned()))
+ .map_err(|error| BaseRelayError::invalid(error.to_string()))
+ })
+ .collect::<Result<Vec<_>, _>>()
+ .map(serde_json::Value::Array)
+ })
+ .collect::<Result<Vec<_>, _>>()?;
+ let content = str::from_utf8(event.content())
+ .map_err(|error| BaseRelayError::invalid(error.to_string()))?;
+ Ok(serde_json::json!([
+ 0,
+ event.pubkey().as_hex_string(),
+ event.created_at().as_u64(),
+ u32::from(event.kind().as_u16()),
+ tags,
+ content
+ ])
+ .to_string())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{
+ is_pocket_nip70_protected_event, pocket_canonical_event_json, pocket_event_id,
+ pocket_event_pubkey, validate_pocket_event_shape, verify_pocket_event_signature,
+ };
+ use crate::pocket_conversion::tangle_event_to_pocket;
+ use tangle_protocol::{Event, EventId, SignatureHex, Tag, event_to_value};
+ use tangle_store_pocket::parse_pocket_event_json;
+ use tangle_test_support::{FixtureKey, tangle_v2_event};
+
+ #[test]
+ fn pocket_event_validation_verifies_valid_and_invalid_signatures() {
+ let event = tangle_v2_event(FixtureKey::Member, 1_714_124_433, 1, Vec::new(), "hello")
+ .expect("event");
+ let pocket = tangle_event_to_pocket(&event).expect("pocket");
+
+ assert_eq!(verify_pocket_event_signature(&pocket), Ok(()));
+
+ let wrong_signature = Event::new(
+ event.id().clone(),
+ event.unsigned().clone(),
+ SignatureHex::new(&"0".repeat(128)).expect("sig"),
+ );
+ let wrong_pocket = tangle_event_to_pocket(&wrong_signature).expect("wrong pocket");
+ assert_eq!(
+ verify_pocket_event_signature(&wrong_pocket)
+ .expect_err("signature")
+ .prefixed_message(),
+ "invalid: event signature verification failed"
+ );
+
+ let wrong_id = Event::new(
+ EventId::new(&"0".repeat(64)).expect("id"),
+ event.unsigned().clone(),
+ event.sig().clone(),
+ );
+ let wrong_id_pocket = tangle_event_to_pocket(&wrong_id).expect("wrong id pocket");
+ assert!(
+ verify_pocket_event_signature(&wrong_id_pocket)
+ .expect_err("id")
+ .prefixed_message()
+ .starts_with("invalid: event id mismatch:")
+ );
+ }
+
+ #[test]
+ fn pocket_event_validation_detects_protected_tags_and_shape_limits() {
+ let event = tangle_v2_event(
+ FixtureKey::Member,
+ 1_714_124_433,
+ 1,
+ vec![Tag::from_parts("-", &[]).expect("protected")],
+ "hello",
+ )
+ .expect("event");
+ let pocket = tangle_event_to_pocket(&event).expect("pocket");
+
+ assert!(is_pocket_nip70_protected_event(&pocket).expect("protected"));
+ assert_eq!(validate_pocket_event_shape(&pocket, 1, 5), Ok(()));
+ assert_eq!(
+ validate_pocket_event_shape(&pocket, 0, 5)
+ .expect_err("tags")
+ .prefixed_message(),
+ "invalid: event tag count exceeds runtime max_event_tags 0"
+ );
+ assert_eq!(
+ validate_pocket_event_shape(&pocket, 1, 4)
+ .expect_err("content")
+ .prefixed_message(),
+ "invalid: event content length exceeds runtime max_content_length 4"
+ );
+ }
+
+ #[test]
+ fn pocket_event_validation_preserves_protocol_canonical_json() {
+ let event = tangle_v2_event(
+ FixtureKey::Member,
+ 1_714_124_433,
+ 1,
+ vec![Tag::from_parts("t", &["market"]).expect("t")],
+ "hello",
+ )
+ .expect("event");
+ let raw = event_to_value(&event).to_string();
+ let pocket = parse_pocket_event_json(raw.as_bytes()).expect("pocket");
+
+ assert_eq!(pocket_event_id(&pocket).expect("id"), event.id().clone());
+ assert_eq!(
+ pocket_event_pubkey(&pocket).expect("pubkey"),
+ event.unsigned().pubkey().clone()
+ );
+ assert_eq!(
+ pocket_canonical_event_json(&pocket).expect("canonical"),
+ event.unsigned().canonical_json()
+ );
+ }
+}
diff --git a/crates/tangle_runtime/src/relay/auth.rs b/crates/tangle_runtime/src/relay/auth.rs
@@ -1,9 +1,17 @@
#![forbid(unsafe_code)]
-use crate::errors::BaseRelayError;
+use crate::{
+ errors::BaseRelayError,
+ pocket_event_validation::{
+ pocket_event_created_at, pocket_event_kind, pocket_event_pubkey,
+ verify_pocket_event_signature,
+ },
+};
use std::collections::BTreeSet;
+use std::str;
use tangle_crypto::verify_event_signature;
use tangle_protocol::{Event, PublicKeyHex, RelayMessage, UnixTimestamp};
+use tangle_store_pocket::PocketEvent;
pub fn generate_auth_challenge() -> Result<String, BaseRelayError> {
let mut bytes = [0_u8; 32];
@@ -115,6 +123,54 @@ impl BaseAuthState {
Ok(pubkey)
}
+ pub(crate) fn authenticate_pocket(
+ &mut self,
+ event: &PocketEvent,
+ now: UnixTimestamp,
+ ) -> Result<PublicKeyHex, BaseRelayError> {
+ verify_pocket_event_signature(event)?;
+ let auth = parse_base_relay_pocket_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.challenge_ttl_seconds)
+ {
+ return Err(BaseRelayError::auth_required("auth challenge expired"));
+ }
+ if auth
+ .created_at()
+ .as_u64()
+ .saturating_add(self.created_at_skew_seconds)
+ < now.as_u64()
+ || auth.created_at().as_u64()
+ > now.as_u64().saturating_add(self.created_at_skew_seconds)
+ {
+ return Err(BaseRelayError::auth_required(
+ "auth event created_at is outside configured skew",
+ ));
+ }
+ let pubkey = auth.pubkey().clone();
+ self.authenticated_pubkeys.insert(pubkey.clone());
+ Ok(pubkey)
+ }
+
pub fn authenticated_pubkeys(&self) -> &BTreeSet<PublicKeyHex> {
&self.authenticated_pubkeys
}
@@ -172,6 +228,32 @@ fn parse_base_relay_auth_event(event: &Event) -> Result<Option<BaseRelayAuthEven
}))
}
+fn parse_base_relay_pocket_auth_event(
+ event: &PocketEvent,
+) -> Result<Option<BaseRelayAuthEvent>, String> {
+ if pocket_event_kind(event)
+ .map_err(|error| error.message().to_owned())?
+ .as_u32()
+ != 22_242
+ {
+ return Ok(None);
+ }
+ let relay = required_single_pocket_tag_value(event, "relay")?;
+ let challenge = required_single_pocket_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: pocket_event_pubkey(event).map_err(|error| error.message().to_owned())?,
+ created_at: pocket_event_created_at(event),
+ relay,
+ challenge,
+ }))
+}
+
fn required_single_tag_value(event: &Event, name: &str) -> Result<String, String> {
let mut matches = event
.unsigned()
@@ -190,6 +272,31 @@ fn required_single_tag_value(event: &Event, name: &str) -> Result<String, String
.ok_or_else(|| format!("tag `{name}` must include a value"))
}
+fn required_single_pocket_tag_value(event: &PocketEvent, name: &str) -> Result<String, String> {
+ let tags = event
+ .tags()
+ .map_err(|error| format!("malformed Pocket event tags: {error}"))?;
+ let mut matched = None;
+ for mut tag in tags.iter() {
+ let Some(tag_name) = tag.next() else {
+ continue;
+ };
+ let tag_name = str::from_utf8(tag_name).map_err(|error| error.to_string())?;
+ if tag_name != name {
+ continue;
+ }
+ if matched.is_some() {
+ return Err(format!("tag `{name}` must not be repeated"));
+ }
+ let value = tag
+ .next()
+ .ok_or_else(|| format!("tag `{name}` must include a value"))
+ .and_then(|value| str::from_utf8(value).map_err(|error| error.to_string()))?;
+ matched = Some(value.to_owned());
+ }
+ matched.ok_or_else(|| format!("tag `{name}` is required"))
+}
+
fn lower_hex(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut output = String::with_capacity(bytes.len() * 2);
@@ -203,6 +310,7 @@ fn lower_hex(bytes: &[u8]) -> String {
#[cfg(test)]
mod tests {
use super::{BaseAuthState, generate_auth_challenge};
+ use crate::pocket_conversion::tangle_event_to_pocket;
use tangle_crypto::RelaySigner;
use tangle_protocol::{Event, EventId, Kind, RelayMessage, Tag, UnixTimestamp, UnsignedEvent};
@@ -479,6 +587,110 @@ mod tests {
}
#[test]
+ fn auth_state_authenticates_pocket_events_without_protocol_conversion() {
+ let mut auth = BaseAuthState::new("wss://relay.radroots.test", 20, 10).expect("auth state");
+ auth.issue_challenge("challenge-a", UnixTimestamp::new(100))
+ .expect("challenge");
+ let owner = signed_auth_event(7, "challenge-a", 105);
+ let admin = signed_auth_event(8, "challenge-a", 106);
+ let owner_pocket = tangle_event_to_pocket(&owner).expect("owner pocket");
+ let admin_pocket = tangle_event_to_pocket(&admin).expect("admin pocket");
+
+ let owner_pubkey = auth
+ .authenticate_pocket(&owner_pocket, UnixTimestamp::new(105))
+ .expect("owner");
+ let admin_pubkey = auth
+ .authenticate_pocket(&admin_pocket, UnixTimestamp::new(106))
+ .expect("admin");
+
+ assert_ne!(owner_pubkey, admin_pubkey);
+ assert!(auth.authenticated_pubkeys().contains(&owner_pubkey));
+ assert!(auth.authenticated_pubkeys().contains(&admin_pubkey));
+ assert_eq!(auth.authenticated_pubkeys().len(), 2);
+ }
+
+ #[test]
+ fn auth_state_rejects_invalid_pocket_auth_events_with_existing_semantics() {
+ let mut auth = BaseAuthState::new("wss://relay.radroots.test", 20, 10).expect("auth state");
+ auth.issue_challenge("challenge-a", UnixTimestamp::new(100))
+ .expect("challenge");
+ let owner = signed_auth_event(7, "challenge-a", 105);
+ let admin = signed_auth_event(8, "challenge-a", 106);
+
+ let wrong_id = tangle_event_to_pocket(&Event::new(
+ EventId::new(&"0".repeat(EventId::HEX_LENGTH)).expect("id"),
+ owner.unsigned().clone(),
+ owner.sig().clone(),
+ ))
+ .expect("wrong id pocket");
+ assert!(
+ auth.authenticate_pocket(&wrong_id, UnixTimestamp::new(105))
+ .expect_err("id")
+ .prefixed_message()
+ .starts_with("invalid: event id mismatch:")
+ );
+
+ let wrong_signature = tangle_event_to_pocket(&Event::new(
+ owner.id().clone(),
+ owner.unsigned().clone(),
+ admin.sig().clone(),
+ ))
+ .expect("wrong signature pocket");
+ assert_eq!(
+ auth.authenticate_pocket(&wrong_signature, UnixTimestamp::new(105))
+ .expect_err("signature")
+ .prefixed_message(),
+ "invalid: event signature verification failed"
+ );
+
+ for (event, now, expected) in [
+ (
+ signed_event(9, 1, auth_tags("challenge-a"), 105),
+ 105,
+ "invalid: AUTH message must contain kind 22242",
+ ),
+ (
+ signed_event(
+ 9,
+ 22_242,
+ auth_tags_for("wss://other.radroots.test", "challenge-a"),
+ 105,
+ ),
+ 105,
+ "auth-required: auth relay does not match canonical relay URL",
+ ),
+ (
+ signed_auth_event(9, "wrong", 105),
+ 105,
+ "auth-required: auth challenge does not match",
+ ),
+ (
+ signed_auth_event(9, "challenge-a", 121),
+ 121,
+ "auth-required: auth challenge expired",
+ ),
+ (
+ signed_auth_event(9, "challenge-a", 94),
+ 105,
+ "auth-required: auth event created_at is outside configured skew",
+ ),
+ (
+ signed_auth_event(9, "challenge-a", 116),
+ 105,
+ "auth-required: auth event created_at is outside configured skew",
+ ),
+ ] {
+ let pocket = tangle_event_to_pocket(&event).expect("pocket");
+ assert_eq!(
+ auth.authenticate_pocket(&pocket, UnixTimestamp::new(now))
+ .expect_err("invalid")
+ .prefixed_message(),
+ expected
+ );
+ }
+ }
+
+ #[test]
fn generated_auth_challenge_is_lowercase_hex_nonce() {
let first = generate_auth_challenge().expect("first");
let second = generate_auth_challenge().expect("second");
diff --git a/crates/tangle_runtime/src/relay/core.rs b/crates/tangle_runtime/src/relay/core.rs
@@ -8,6 +8,9 @@ use crate::pocket_conversion::{
pocket_event_id, pocket_event_to_tangle, pocket_pubkey, tangle_event_to_pocket,
tangle_filter_to_pocket,
};
+use crate::pocket_event_validation::{
+ pocket_event_id as pocket_runtime_event_id, validate_pocket_event_shape,
+};
use crate::relay::{
auth::BaseAuthState,
live::{CloseResult, LiveSubscriptionSet},
@@ -23,7 +26,8 @@ use tangle_groups::{
};
use tangle_protocol::{ClientMessage, Event, Filter, RelayMessage, SubscriptionId, UnixTimestamp};
use tangle_store_pocket::{
- PocketHll8, PocketQueryConfig, PocketScreenResult, PocketStoreConfig, PocketStoreHandle,
+ PocketEvent, PocketHll8, PocketQueryConfig, PocketScreenResult, PocketStoreConfig,
+ PocketStoreHandle,
};
pub(crate) const NEGENTROPY_DISABLED_MESSAGE: &str = "blocked: Negentropy sync is disabled";
@@ -425,6 +429,10 @@ impl BaseRelayLimits {
Ok(())
}
+ pub(crate) fn validate_pocket_event(&self, event: &PocketEvent) -> Result<(), BaseRelayError> {
+ validate_pocket_event_shape(event, self.max_event_tags, self.max_content_length)
+ }
+
pub fn validate_subscription_id(
&self,
subscription_id: &SubscriptionId,
@@ -718,6 +726,38 @@ impl BaseRelay {
})
}
+ pub(crate) fn handle_pocket_auth_with_limits(
+ limits: BaseRelayLimits,
+ event: &PocketEvent,
+ auth: &mut BaseAuthState,
+ now: UnixTimestamp,
+ ) -> Vec<RelayMessage> {
+ let event_id =
+ pocket_runtime_event_id(event).expect("Pocket event id is valid hex by construction");
+ if let Err(error) = limits.validate_pocket_event(event) {
+ return vec![RelayMessage::Ok {
+ event_id,
+ accepted: false,
+ message: error.prefixed_message(),
+ }];
+ }
+ auth.authenticate_pocket(event, now)
+ .map(|_| {
+ vec![RelayMessage::Ok {
+ event_id: event_id.clone(),
+ accepted: true,
+ message: String::new(),
+ }]
+ })
+ .unwrap_or_else(|error| {
+ vec![RelayMessage::Ok {
+ event_id,
+ accepted: false,
+ message: error.prefixed_message(),
+ }]
+ })
+ }
+
pub fn handle_event(&self, event: Event) -> Result<RelayMessage, BaseRelayError> {
self.handle_event_with_group_auth(event, &GroupAuthContext::unauthenticated())
.map(BaseRelayEventWrite::into_message)
diff --git a/crates/tangle_runtime/src/runtime.rs b/crates/tangle_runtime/src/runtime.rs
@@ -9,6 +9,9 @@ use crate::{
logging,
ops::{BaseRelayReadinessHandle, BaseRelayReadinessState},
pocket_conversion::{pocket_event_to_tangle, pocket_filter_to_tangle},
+ pocket_event_validation::{
+ is_pocket_nip70_protected_event, pocket_event_id, pocket_event_pubkey,
+ },
rate_limits::{
TangleQueryRateLimitConfig, TangleRateLimitDecision, TangleRateLimitKey,
TangleRateLimitQueryClass, TangleRateLimitRule, TangleRateLimitScope, TangleRateLimiter,
@@ -41,7 +44,7 @@ use tangle_groups::{
use tangle_protocol::{
Event, EventId, Filter, Kind, PublicKeyHex, RelayMessage, SubscriptionId, UnixTimestamp,
};
-use tangle_store_pocket::PocketStoreHandle;
+use tangle_store_pocket::{PocketEvent, PocketOwnedFilter, PocketStoreHandle};
use tokio::sync::watch;
pub struct TangleRuntime {
@@ -407,57 +410,54 @@ impl TangleRuntimeShared {
})
}
- fn rate_limit_auth_attempt(
+ fn rate_limit_auth_attempt_pocket(
&self,
- event: &Event,
+ event: &PocketEvent,
context: TangleClientRateLimitContext,
now: UnixTimestamp,
- ) -> Option<RelayMessage> {
+ ) -> Result<Option<RelayMessage>, BaseRelayError> {
let rules = self.config.rate_limits().auth();
if let Some(peer_ip) = context.peer_ip
- && let Some(message) = self.rate_limit_ok(
+ && let Some(message) = self.rate_limit_ok_pocket(
event,
TangleRateLimitKey::ip(TangleRateLimitScope::Auth, peer_ip),
rules.per_ip(),
"auth ip",
now,
- )
+ )?
{
- return Some(message);
+ return Ok(Some(message));
}
- self.rate_limit_ok(
+ self.rate_limit_ok_pocket(
event,
- TangleRateLimitKey::pubkey(
- TangleRateLimitScope::Auth,
- event.unsigned().pubkey().clone(),
- ),
+ TangleRateLimitKey::pubkey(TangleRateLimitScope::Auth, pocket_event_pubkey(event)?),
rules.per_pubkey(),
"auth pubkey",
now,
)
}
- fn rate_limit_auth_failure(
+ fn rate_limit_auth_failure_pocket(
&self,
- event: &Event,
+ event: &PocketEvent,
context: TangleClientRateLimitContext,
now: UnixTimestamp,
- ) -> Option<RelayMessage> {
+ ) -> Result<Option<RelayMessage>, BaseRelayError> {
let rules = self.config.rate_limits().auth();
if let Some(peer_ip) = context.peer_ip
- && let Some(message) = self.rate_limit_ok(
+ && let Some(message) = self.rate_limit_ok_pocket(
event,
TangleRateLimitKey::auth_failure(Some(peer_ip), None),
rules.failures_per_ip(),
"auth failure ip",
now,
- )
+ )?
{
- return Some(message);
+ return Ok(Some(message));
}
- self.rate_limit_ok(
+ self.rate_limit_ok_pocket(
event,
- TangleRateLimitKey::auth_failure(None, Some(event.unsigned().pubkey().clone())),
+ TangleRateLimitKey::auth_failure(None, Some(pocket_event_pubkey(event)?)),
rules.failures(),
"auth failure",
now,
@@ -789,6 +789,31 @@ impl TangleRuntimeShared {
}
}
}
+
+ fn rate_limit_ok_pocket(
+ &self,
+ event: &PocketEvent,
+ key: TangleRateLimitKey,
+ rule: TangleRateLimitRule,
+ label: &'static str,
+ now: UnixTimestamp,
+ ) -> Result<Option<RelayMessage>, BaseRelayError> {
+ Ok(match self.rate_limiter.record(key, rule, now) {
+ TangleRateLimitDecision::Allowed { .. } => None,
+ TangleRateLimitDecision::Rejected { reset_at } => {
+ self.metrics.record_rate_limit_rejection();
+ logging::log_rate_limit_rejected(label, "event", reset_at);
+ Some(RelayMessage::Ok {
+ event_id: pocket_event_id(event)?,
+ accepted: false,
+ message: BaseRelayError::rate_limited(format!(
+ "{label} rate limit exceeded until {reset_at}"
+ ))
+ .prefixed_message(),
+ })
+ }
+ })
+ }
}
#[derive(Clone)]
@@ -815,6 +840,26 @@ impl TangleRuntimeHandle {
self.inner.config.auth_state()
}
+ pub async fn handle_count_pocket(
+ &self,
+ subscription_id: SubscriptionId,
+ filters: Vec<PocketOwnedFilter>,
+ auth: &mut BaseAuthState,
+ now: UnixTimestamp,
+ ) -> Result<Vec<RelayMessage>, BaseRelayError> {
+ self.handle_client_message_with_rate_limit_context(
+ RuntimeClientMessage::Count {
+ subscription_id,
+ filters,
+ search_present: false,
+ },
+ auth,
+ TangleClientRateLimitContext::default(),
+ now,
+ )
+ .await
+ }
+
#[cfg(test)]
pub(crate) async fn handle_client_message(
&self,
@@ -876,6 +921,14 @@ impl TangleRuntimeHandle {
match message {
RuntimeClientMessage::Event(pocket_event) => {
let event = pocket_event_to_tangle(&pocket_event)?;
+ debug_assert_eq!(
+ is_pocket_nip70_protected_event(&pocket_event)?,
+ event
+ .unsigned()
+ .tags()
+ .iter()
+ .any(|tag| tag.name().as_str() == "-")
+ );
let started_at = Instant::now();
let event_id = event.id().clone();
let is_group_event = self.inner.is_group_event(&event);
@@ -1023,36 +1076,42 @@ impl TangleRuntimeHandle {
Ok(vec![report.into_message()])
}
RuntimeClientMessage::Auth(pocket_event) => {
- let event = pocket_event_to_tangle(&pocket_event)?;
- if let Err(error) = self.inner.limits.base_relay_limits().validate_event(&event) {
+ let event_id = pocket_event_id(&pocket_event)?;
+ if let Err(error) = self
+ .inner
+ .limits
+ .base_relay_limits()
+ .validate_pocket_event(&pocket_event)
+ {
self.inner.metrics.record_auth_failure();
return Ok(vec![RelayMessage::Ok {
- event_id: event.id().clone(),
+ event_id,
accepted: false,
message: error.prefixed_message(),
}]);
}
- if let Some(message) =
- self.inner
- .rate_limit_auth_attempt(&event, rate_limit_context, now)
- {
+ if let Some(message) = self.inner.rate_limit_auth_attempt_pocket(
+ &pocket_event,
+ rate_limit_context,
+ now,
+ )? {
self.inner.metrics.record_auth_failure();
return Ok(vec![message]);
}
- let event_for_failure = event.clone();
- let replies = BaseRelay::handle_auth_with_limits(
+ let event_for_failure = pocket_event.clone();
+ let replies = BaseRelay::handle_pocket_auth_with_limits(
self.inner.limits.base_relay_limits(),
- event,
+ &pocket_event,
auth,
now,
);
if auth_response_failed(&replies) {
self.inner.metrics.record_auth_failure();
- if let Some(message) = self.inner.rate_limit_auth_failure(
+ if let Some(message) = self.inner.rate_limit_auth_failure_pocket(
&event_for_failure,
rate_limit_context,
now,
- ) {
+ )? {
return Ok(vec![message]);
}
} else {