commit 7e62f9971ce65a1522a0586ed9671bd65ecb086c
parent f3510f37e41d893cb41bde73a4e46f170cc6e497
Author: triesap <tyson@radroots.org>
Date: Sun, 14 Jun 2026 08:20:40 -0700
nip70: require author auth for protected events
- Reject events carrying the protected-event dash tag unless the authenticated client pubkey set contains the event author.
- Keep NIP-70 advertised only after enforcement is present and assert the NIP-11 builder includes it.
- Replace the phase2 NIP-70 placeholder with an active protected-event acceptance test covering unauthenticated rejection and author-auth acceptance.
- Validated with cargo fmt --all -- --check, cargo check --workspace --all-targets, cargo test --workspace, and cargo clippy --workspace --all-targets -- -D warnings.
Diffstat:
3 files changed, 99 insertions(+), 2 deletions(-)
diff --git a/crates/tangle_runtime/src/nip11.rs b/crates/tangle_runtime/src/nip11.rs
@@ -210,6 +210,7 @@ mod tests {
assert!(document.supported_nips.contains(&29));
assert!(document.supported_nips.contains(&45));
+ assert!(document.supported_nips.contains(&70));
assert!(document.relay_self().is_some());
assert_eq!(document.description.as_deref(), Some("Tangle v2 relay"));
assert!(!disabled.supported_nips.contains(&29));
diff --git a/crates/tangle_runtime/src/relay/core.rs b/crates/tangle_runtime/src/relay/core.rs
@@ -55,6 +55,14 @@ impl BaseRelayEventWrite {
}
}
+fn is_nip70_protected_event(event: &Event) -> bool {
+ event
+ .unsigned()
+ .tags()
+ .iter()
+ .any(|tag| tag.name().as_str() == "-")
+}
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BaseRelayShutdownReport {
closed_subscriptions: usize,
@@ -529,6 +537,15 @@ impl BaseRelay {
format!("invalid: {error}"),
)));
}
+ if is_nip70_protected_event(&event) && !auth.contains(event.unsigned().pubkey()) {
+ return Ok(BaseRelayEventWrite::unstored(ok_rejected(
+ event_id,
+ BaseRelayError::auth_required(
+ "protected event requires authenticated event author",
+ )
+ .prefixed_message(),
+ )));
+ }
let group_limits = self
.groups
.as_ref()
@@ -1224,6 +1241,39 @@ mod tests {
}
#[test]
+ fn base_relay_enforces_nip70_protected_event_author_auth() {
+ let mut relay = test_relay("base-relay-nip70-protected", 8);
+ let protected = signed_public_event(
+ 7,
+ 1,
+ vec![Tag::from_parts("-", &[]).expect("protected")],
+ "protected",
+ );
+
+ assert_eq!(
+ rejected_message(relay.handle_event(protected.clone()).expect("unauth")),
+ "auth-required: protected event requires authenticated event author"
+ );
+ assert_eq!(count_kind(&relay, 1), 0);
+ assert_eq!(
+ rejected_message(
+ relay
+ .handle_event_with_auth(protected.clone(), &authenticated_state(8))
+ .expect("wrong auth")
+ ),
+ "auth-required: protected event requires authenticated event author"
+ );
+ assert_eq!(count_kind(&relay, 1), 0);
+ assert_accepted(
+ relay
+ .handle_event_with_auth(protected.clone(), &authenticated_state(7))
+ .expect("author auth"),
+ &protected,
+ );
+ assert_eq!(count_kind(&relay, 1), 1);
+ }
+
+ #[test]
fn base_relay_rejects_group_marked_events_before_group_service() {
let mut relay = test_relay("base-relay-group-reject", 4);
let event = signed_public_event(
diff --git a/crates/tangle_runtime/tests/phase2_acceptance_targets.rs b/crates/tangle_runtime/tests/phase2_acceptance_targets.rs
@@ -23,6 +23,7 @@ use tangle_protocol::{
};
use tangle_runtime::{
config::{BaseRelayRuntimeConfig, parse_base_relay_runtime_config_json},
+ nip11::BaseRelayInfoConfig,
relay::auth::BaseAuthState,
runtime::TangleRuntime,
server::serve_listener_until_shutdown,
@@ -286,9 +287,54 @@ fn auth_rejects_events_outside_created_at_skew() {
}
#[test]
-#[ignore = "phase2 target: nip70 enforcement"]
fn protected_events_require_author_auth_before_nip70_is_advertised() {
- pending("events with a dash tag require AUTH as event author before NIP-70 advertisement");
+ let root = temp_root("acceptance-nip70");
+ let _ = std::fs::remove_dir_all(&root);
+ let config = runtime_config(&root, SocketAddr::from(([127, 0, 0, 1], 0)));
+ let document = BaseRelayInfoConfig::new("tangle", config.groups().clone())
+ .expect("info config")
+ .build_document()
+ .expect("document");
+ let mut relay = config.open_relay().expect("relay");
+ let protected = tangle_v2_event(
+ FixtureKey::Member,
+ 1_714_124_433,
+ 1,
+ vec![Tag::from_parts("-", &[]).expect("protected")],
+ "protected",
+ )
+ .expect("protected event");
+ let mut auth = BaseAuthState::new(TANGLE_V2_RELAY_URL, 300, 10).expect("auth");
+ auth.issue_challenge("challenge-a", UnixTimestamp::new(1_714_124_433))
+ .expect("challenge");
+ auth.authenticate(
+ &tangle_v2_auth_event(FixtureKey::Member, "challenge-a", 1_714_124_433).expect("auth"),
+ UnixTimestamp::new(1_714_124_433),
+ )
+ .expect("author auth");
+
+ assert!(document.supported_nips.contains(&70));
+ assert_eq!(
+ relay.handle_event(protected.clone()).expect("unauth"),
+ RelayMessage::Ok {
+ event_id: protected.id().clone(),
+ accepted: false,
+ message: "auth-required: protected event requires authenticated event author"
+ .to_owned()
+ }
+ );
+ assert_eq!(
+ relay
+ .handle_event_with_auth(protected.clone(), &auth)
+ .expect("author write"),
+ RelayMessage::Ok {
+ event_id: protected.id().clone(),
+ accepted: true,
+ message: String::new()
+ }
+ );
+
+ let _ = std::fs::remove_dir_all(root);
}
#[test]