tangle


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

commit b21160402bf40e8478c6086d1a5f2796613d37e0
parent caed34f071bd8b995d0804b113713be39d126d2a
Author: triesap <tyson@radroots.org>
Date:   Sun, 14 Jun 2026 08:32:34 -0700

nip11: derive supported nips

Diffstat:
Mcrates/tangle_bench/src/bin/tangle_benchmark_report.rs | 9++++++---
Mcrates/tangle_runtime/src/lib.rs | 1-
Mcrates/tangle_runtime/src/nip11.rs | 31++++++++++++++++++++-----------
Mcrates/tangle_runtime/tests/base_relay_v2.rs | 10+++-------
Mcrates/tangle_runtime/tests/phase2_acceptance_targets.rs | 55+++++++++++++++++++++++++++++++++++++++++++++++++++----
5 files changed, 80 insertions(+), 26 deletions(-)

diff --git a/crates/tangle_bench/src/bin/tangle_benchmark_report.rs b/crates/tangle_bench/src/bin/tangle_benchmark_report.rs @@ -6,7 +6,7 @@ use std::path::{Path, PathBuf}; use std::process::Command; use std::time::{SystemTime, UNIX_EPOCH}; use tangle_bench::{BenchDatasetConfig, BenchmarkRunReport}; -use tangle_runtime::TANGLE_SUPPORTED_NIPS; +use tangle_runtime::nip11::supported_nips_for_group_capability; struct BenchmarkReportArgs { output_root: PathBuf, @@ -45,9 +45,12 @@ fn run() -> Result<Option<PathBuf>, String> { .map_err(|error| error.to_string())?; let mut summary = report.summary_json(&args.run_id, &artifact_dir); + let supported_nips = supported_nips_for_group_capability(true); + let supported_nips_count = supported_nips.len(); summary["supported_nips_audit"] = serde_json::json!({ - "supported_nips": TANGLE_SUPPORTED_NIPS, - "count": TANGLE_SUPPORTED_NIPS.len() + "groups_enabled": true, + "supported_nips": supported_nips, + "count": supported_nips_count }); summary["run_identity"] = serde_json::json!({ "git_commit": git_short_commit(), diff --git a/crates/tangle_runtime/src/lib.rs b/crates/tangle_runtime/src/lib.rs @@ -21,7 +21,6 @@ use config::{BaseRelayRuntimeConfig, parse_base_relay_runtime_config_json}; use errors::BaseRelayError; use runtime::TangleRuntime; -pub const TANGLE_SUPPORTED_NIPS: [u16; 6] = [1, 11, 29, 42, 45, 70]; pub const TANGLE_RELAY_SOFTWARE: &str = "https://github.com/radrootslabs/tangle"; pub const TANGLE_RELAY_VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/crates/tangle_runtime/src/nip11.rs b/crates/tangle_runtime/src/nip11.rs @@ -16,7 +16,21 @@ use tangle_crypto::RelaySigner; use tangle_groups::GroupRuntimeConfig; use tangle_protocol::PublicKeyHex; -pub const BASE_RELAY_SUPPORTED_NIPS: [u16; 5] = [1, 11, 42, 45, 70]; +const ALWAYS_SUPPORTED_NIPS: [u16; 5] = [1, 11, 42, 45, 70]; +const GROUP_SUPPORTED_NIP: u16 = 29; + +pub fn supported_nips_for_runtime(runtime: &BaseRelayRuntimeConfig) -> Vec<u16> { + supported_nips_for_group_capability(runtime.groups().enabled()) +} + +pub fn supported_nips_for_group_capability(groups_enabled: bool) -> Vec<u16> { + let mut supported_nips = ALWAYS_SUPPORTED_NIPS.to_vec(); + if groups_enabled { + supported_nips.push(GROUP_SUPPORTED_NIP); + supported_nips.sort_unstable(); + } + supported_nips +} #[derive(Debug, Clone, PartialEq, Eq)] pub struct BaseRelayInfoConfig { @@ -30,6 +44,7 @@ pub struct BaseRelayInfoConfig { version: String, payment_required: bool, restricted_writes: bool, + supported_nips: Vec<u16>, } impl BaseRelayInfoConfig { @@ -52,6 +67,7 @@ impl BaseRelayInfoConfig { version: crate::TANGLE_RELAY_VERSION.to_owned(), payment_required: false, restricted_writes: true, + supported_nips: supported_nips_for_runtime(runtime), }) } @@ -72,18 +88,13 @@ impl BaseRelayInfoConfig { pub fn build_document(&self) -> Result<BaseRelayInfoDocument, BaseRelayError> { let relay_self = relay_self_from_groups(&self.groups)?; - let mut supported_nips = BASE_RELAY_SUPPORTED_NIPS.to_vec(); - if self.groups.enabled() { - supported_nips.push(29); - supported_nips.sort_unstable(); - } Ok(BaseRelayInfoDocument { name: self.name.clone(), description: self.description.clone(), contact: self.contact.clone(), icon: self.icon.clone(), relay_self: relay_self.map(|pubkey| pubkey.as_str().to_owned()), - supported_nips, + supported_nips: self.supported_nips.clone(), software: self.software.clone(), version: self.version.clone(), limitation: BaseRelayInfoLimitationDocument { @@ -233,9 +244,7 @@ mod tests { .build_document() .expect("disabled"); - assert!(document.supported_nips.contains(&29)); - assert!(document.supported_nips.contains(&45)); - assert!(document.supported_nips.contains(&70)); + assert_eq!(document.supported_nips, vec![1, 11, 29, 42, 45, 70]); assert!(document.relay_self().is_some()); assert_eq!(document.description.as_deref(), Some("Tangle v2 relay")); assert_eq!(document.limitation.max_message_length, 1_048_576); @@ -249,7 +258,7 @@ mod tests { assert!(!document.limitation.payment_required); assert!(document.limitation.restricted_writes); assert_eq!(document.limitation.default_limit, 100); - assert!(!disabled.supported_nips.contains(&29)); + assert_eq!(disabled.supported_nips, vec![1, 11, 42, 45, 70]); assert!(disabled.relay_self().is_none()); } diff --git a/crates/tangle_runtime/tests/base_relay_v2.rs b/crates/tangle_runtime/tests/base_relay_v2.rs @@ -17,7 +17,7 @@ use tangle_protocol::{ use tangle_runtime::{ config::{BaseRelayRuntimeConfig, parse_base_relay_runtime_config_json}, groups::{GroupCheckpointStatus, validate_group_extra_tables}, - nip11::{BASE_RELAY_SUPPORTED_NIPS, BaseRelayInfoConfig}, + nip11::BaseRelayInfoConfig, relay::{ auth::BaseAuthState, core::{BaseRelay, BaseRelayLimitSettings, BaseRelayLimits}, @@ -87,11 +87,7 @@ fn nip11_integration_reports_group_contracts() { .build_document() .expect("disabled"); - assert!(BASE_RELAY_SUPPORTED_NIPS.contains(&1)); - assert!(document.supported_nips.contains(&29)); - assert!(document.supported_nips.contains(&42)); - assert!(document.supported_nips.contains(&45)); - assert!(document.supported_nips.contains(&70)); + assert_eq!(document.supported_nips, vec![1, 11, 29, 42, 45, 70]); assert!(!document.supported_nips.contains(&50)); assert!(!document.supported_nips.contains(&77)); assert!(!document.supported_nips.contains(&99)); @@ -107,7 +103,7 @@ fn nip11_integration_reports_group_contracts() { assert!(!document.limitation.payment_required); assert!(document.limitation.restricted_writes); assert_eq!(document.limitation.default_limit, 100); - assert!(!disabled.supported_nips.contains(&29)); + assert_eq!(disabled.supported_nips, vec![1, 11, 42, 45, 70]); assert!(disabled.relay_self().is_none()); } diff --git a/crates/tangle_runtime/tests/phase2_acceptance_targets.rs b/crates/tangle_runtime/tests/phase2_acceptance_targets.rs @@ -244,10 +244,53 @@ async fn websocket_clients_use_nip01_nip42_and_nip45_flows() { let _ = std::fs::remove_dir_all(root); } -#[test] -#[ignore = "phase2 target: nip11 truthfulness"] -fn nip11_includes_cors_headers_and_truthful_supported_nips() { - pending("NIP-11 must include CORS headers and advertise only enforced NIPs"); +#[tokio::test] +async fn nip11_includes_cors_headers_and_truthful_supported_nips() { + let root = temp_root("acceptance-nip11"); + let _ = std::fs::remove_dir_all(&root); + let listener = TcpListener::bind("127.0.0.1:0").await.expect("listener"); + let address = listener.local_addr().expect("address"); + let runtime = TangleRuntime::open(runtime_config(&root, address)).expect("runtime"); + let shutdown = runtime.shutdown_signal().clone(); + let task = tokio::spawn(serve_listener_until_shutdown(runtime, listener)); + + let response = wait_for_http_ok(address, "/", Some("application/nostr+json")).await; + let lower = response.to_ascii_lowercase(); + assert!(lower.contains("content-type: application/nostr+json")); + assert!(lower.contains("access-control-allow-origin: *")); + assert!(lower.contains("access-control-allow-headers: *")); + assert!(lower.contains("access-control-allow-methods: *")); + + let document = serde_json::from_str::<Value>(response_body(&response)).expect("nip11 json"); + assert_eq!(document["supported_nips"], json!([1, 11, 29, 42, 45, 70])); + assert!( + !document["supported_nips"] + .as_array() + .expect("supported nips") + .contains(&json!(50)) + ); + assert!( + !document["supported_nips"] + .as_array() + .expect("supported nips") + .contains(&json!(77)) + ); + assert!( + !document["supported_nips"] + .as_array() + .expect("supported nips") + .contains(&json!(99)) + ); + + shutdown.request_shutdown(); + let report = timeout(Duration::from_secs(2), task) + .await + .expect("shutdown timeout") + .expect("task") + .expect("serve"); + assert_eq!(report.listen_addr(), address); + + let _ = std::fs::remove_dir_all(root); } #[test] @@ -696,6 +739,10 @@ fn http_get(address: SocketAddr, path: &str, accept: Option<&str>) -> std::io::R Ok(response) } +fn response_body(response: &str) -> &str { + response.split_once("\r\n\r\n").expect("response body").1 +} + fn temp_root(name: &str) -> PathBuf { std::env::temp_dir().join(format!("tangle-runtime-{name}-{}", std::process::id())) }