tangle


git clone https://radroots.dev/git/tangle.git
Log | Files | Refs | README | LICENSE

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:
Mcrates/tangle_runtime/src/nip11.rs | 1+
Mcrates/tangle_runtime/src/relay/core.rs | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/tangle_runtime/tests/phase2_acceptance_targets.rs | 50++++++++++++++++++++++++++++++++++++++++++++++++--
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]