commit b21160402bf40e8478c6086d1a5f2796613d37e0
parent caed34f071bd8b995d0804b113713be39d126d2a
Author: triesap <tyson@radroots.org>
Date: Sun, 14 Jun 2026 08:32:34 -0700
nip11: derive supported nips
Diffstat:
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()))
}