ops_truthfulness.rs (13144B)
1 #![forbid(unsafe_code)] 2 3 use serde_json::json; 4 use std::path::{Path, PathBuf}; 5 use tangle_crypto::RelaySigner; 6 use tangle_protocol::{ 7 Event, EventId, Kind, PublicKeyHex, RelayMessage, SignatureHex, Tag, UnixTimestamp, 8 UnsignedEvent, event_to_value, 9 }; 10 use tangle_runtime::{ 11 config::{BaseRelayRuntimeConfig, parse_base_relay_runtime_config_json}, 12 errors::BaseRelayError, 13 logging::{TANGLE_LOG_REDACTED, TangleLogRedactor}, 14 nip11::BaseRelayInfoConfig, 15 ops::BaseRelayReadinessCheckStatus, 16 rate_limits::{TangleRateLimitKey, TangleRateLimitScope, TangleRateLimiter}, 17 relay::{auth::BaseAuthState, core::BaseRelay}, 18 runtime::RelayRuntime, 19 }; 20 use tangle_store_pocket::parse_pocket_event_json; 21 use tangle_store_pocket::{PocketEvent, PocketKind, PocketOwnedEvent, PocketOwnedTags, PocketTime}; 22 use tangle_test_support::{FixtureKey, TANGLE_V2_RELAY_SECRET_HEX, TANGLE_V2_RELAY_URL}; 23 24 trait BaseRelayEventTestExt { 25 fn handle_event(&self, event: Event) -> Result<RelayMessage, BaseRelayError>; 26 27 fn handle_event_with_auth( 28 &self, 29 event: Event, 30 auth: &BaseAuthState, 31 ) -> Result<RelayMessage, BaseRelayError>; 32 } 33 34 impl BaseRelayEventTestExt for BaseRelay { 35 fn handle_event(&self, event: Event) -> Result<RelayMessage, BaseRelayError> { 36 let raw = serde_json::to_vec(&event_to_value(&event)).expect("event JSON"); 37 let pocket = parse_pocket_event_json(&raw).expect("pocket event"); 38 self.handle_pocket_event(&pocket) 39 } 40 41 fn handle_event_with_auth( 42 &self, 43 event: Event, 44 auth: &BaseAuthState, 45 ) -> Result<RelayMessage, BaseRelayError> { 46 let raw = serde_json::to_vec(&event_to_value(&event)).expect("event JSON"); 47 let pocket = parse_pocket_event_json(&raw).expect("pocket event"); 48 self.handle_pocket_event_with_auth(&pocket, auth) 49 } 50 } 51 52 fn authenticate_pocket_event_for_test( 53 auth: &mut BaseAuthState, 54 event: &Event, 55 now: UnixTimestamp, 56 ) -> Result<(), BaseRelayError> { 57 let raw = serde_json::to_vec(&event_to_value(event)).expect("event JSON"); 58 let pocket = parse_pocket_event_json(&raw).expect("pocket event"); 59 auth.authenticate_pocket(&pocket, now).map(|_| ()) 60 } 61 62 fn tangle_v2_event( 63 key: FixtureKey, 64 created_at: u64, 65 kind: u64, 66 tags: Vec<Tag>, 67 content: &str, 68 ) -> Result<Event, String> { 69 let event = ops_pocket_event(key, created_at, kind, tags, content); 70 ops_pocket_event_to_protocol(&event) 71 } 72 73 fn tangle_v2_auth_event( 74 key: FixtureKey, 75 challenge: &str, 76 created_at: u64, 77 ) -> Result<Event, String> { 78 tangle_v2_event( 79 key, 80 created_at, 81 22_242, 82 vec![ 83 Tag::from_parts("relay", &[TANGLE_V2_RELAY_URL])?, 84 Tag::from_parts("challenge", &[challenge])?, 85 ], 86 "", 87 ) 88 } 89 90 fn ops_pocket_event( 91 key: FixtureKey, 92 created_at: u64, 93 kind: u64, 94 tags: Vec<Tag>, 95 content: &str, 96 ) -> PocketOwnedEvent { 97 let tags = ops_pocket_tags_from_protocol(&tags); 98 let secret = format!("{:02x}", fixture_secret_byte(key)).repeat(32); 99 RelaySigner::from_secret_hex(&secret) 100 .expect("signer") 101 .sign_pocket_event( 102 PocketKind::from_u16(u16::try_from(kind).expect("pocket kind")), 103 &tags, 104 PocketTime::from_u64(created_at), 105 content.as_bytes(), 106 ) 107 .expect("pocket event") 108 } 109 110 fn ops_pocket_tags_from_protocol(tags: &[Tag]) -> PocketOwnedTags { 111 let parts = tags 112 .iter() 113 .map(|tag| tag.values().iter().map(String::as_str).collect::<Vec<_>>()) 114 .collect::<Vec<_>>(); 115 PocketOwnedTags::new(&parts).expect("pocket tags") 116 } 117 118 fn ops_pocket_event_to_protocol(event: &PocketEvent) -> Result<Event, String> { 119 let tags = event 120 .tags() 121 .map_err(|error| error.to_string())? 122 .iter() 123 .map(|tag| { 124 Tag::new( 125 tag.map(|value| { 126 std::str::from_utf8(value) 127 .map(str::to_owned) 128 .map_err(|error| error.to_string()) 129 }) 130 .collect::<Result<Vec<_>, _>>()?, 131 ) 132 .map_err(|error| error.to_string()) 133 }) 134 .collect::<Result<Vec<_>, _>>()?; 135 Ok(Event::new( 136 EventId::new(&event.id().as_hex_string()).map_err(|error| error.to_string())?, 137 UnsignedEvent::new( 138 PublicKeyHex::new(&event.pubkey().as_hex_string()) 139 .map_err(|error| error.to_string())?, 140 UnixTimestamp::new(event.created_at().as_u64()), 141 Kind::new(u64::from(event.kind().as_u16())).map_err(|error| error.to_string())?, 142 tags, 143 std::str::from_utf8(event.content()).map_err(|error| error.to_string())?, 144 ), 145 SignatureHex::new(&event.sig().to_string()).map_err(|error| error.to_string())?, 146 )) 147 } 148 149 fn fixture_secret_byte(key: FixtureKey) -> u8 { 150 match key { 151 FixtureKey::Relay => 9, 152 FixtureKey::Owner => 10, 153 FixtureKey::Admin => 11, 154 FixtureKey::Member => 12, 155 FixtureKey::Outsider => 13, 156 } 157 } 158 159 #[test] 160 fn operations_surfaces_match_enforced_runtime_contracts() { 161 let root = temp_root("ops-truthfulness"); 162 let _ = std::fs::remove_dir_all(&root); 163 let config = runtime_config(&root); 164 let document = BaseRelayInfoConfig::new("tangle", &config) 165 .expect("info config") 166 .build_document() 167 .expect("document"); 168 169 assert_eq!(document.supported_nips, vec![1, 11, 29, 42, 45, 70]); 170 assert!(!document.supported_nips.contains(&77)); 171 assert_eq!(document.limitation.max_message_length, 1_048_576); 172 assert_eq!(document.limitation.max_subscriptions, 64); 173 assert_eq!(document.limitation.max_filters, 10); 174 assert_eq!(document.limitation.max_limit, 500); 175 assert_eq!(document.limitation.max_query_complexity, 2_048); 176 assert_eq!(document.limitation.default_limit, 100); 177 assert!(document.limitation.restricted_writes); 178 assert!(!document.retention.physical_erasure); 179 assert!(!document.retention.compaction_guarantee); 180 181 let redactor = TangleLogRedactor::from_runtime_config(&config); 182 assert_eq!( 183 redactor.redact(format!("relay secret {TANGLE_V2_RELAY_SECRET_HEX}")), 184 format!("relay secret {TANGLE_LOG_REDACTED}") 185 ); 186 assert!(!format!("{redactor:?}").contains(TANGLE_V2_RELAY_SECRET_HEX)); 187 188 let rate_limits = config.rate_limits(); 189 assert_eq!(rate_limits.auth().failures().max_hits(), 1); 190 assert_eq!(rate_limits.req().broad().window_seconds(), 60); 191 let limiter = TangleRateLimiter::new(); 192 let key = 193 TangleRateLimitKey::pubkey(TangleRateLimitScope::Auth, FixtureKey::Member.public_key()); 194 assert!( 195 limiter 196 .record( 197 key.clone(), 198 rate_limits.auth().failures(), 199 UnixTimestamp::new(100) 200 ) 201 .is_allowed() 202 ); 203 assert!( 204 !limiter 205 .record( 206 key.clone(), 207 rate_limits.auth().failures(), 208 UnixTimestamp::new(101) 209 ) 210 .is_allowed() 211 ); 212 213 let runtime = RelayRuntime::open(config.clone()).expect("runtime"); 214 let pre_bind = runtime.readiness_state().response(); 215 assert_eq!(pre_bind.status, "not_ready"); 216 assert_eq!(pre_bind.checks.server_bind, "not_ready"); 217 assert_eq!(pre_bind.checks.group_projection, "ready"); 218 assert_eq!(pre_bind.checks.group_outbox_replay, "ready"); 219 assert_eq!(pre_bind.checks.event_bus, "ready"); 220 let bound = runtime 221 .readiness_state() 222 .clone() 223 .with_server_bind(BaseRelayReadinessCheckStatus::Ready) 224 .response(); 225 assert_eq!(bound.status, "ready"); 226 assert_eq!(bound.checks.server_bind, "ready"); 227 228 let relay = config.open_relay().expect("relay"); 229 let protected = tangle_v2_event( 230 FixtureKey::Member, 231 1_714_124_433, 232 1, 233 vec![Tag::from_parts("-", &[]).expect("protected")], 234 "protected", 235 ) 236 .expect("protected event"); 237 assert_eq!( 238 relay.handle_event(protected.clone()).expect("unauth"), 239 RelayMessage::Ok { 240 event_id: protected.id().clone(), 241 accepted: false, 242 message: "auth-required: protected event requires authenticated event author" 243 .to_owned() 244 } 245 ); 246 247 let mut auth = BaseAuthState::new(TANGLE_V2_RELAY_URL, 300, 600).expect("auth"); 248 auth.issue_challenge("challenge-a", UnixTimestamp::new(1_714_124_433)) 249 .expect("challenge"); 250 authenticate_pocket_event_for_test( 251 &mut auth, 252 &tangle_v2_auth_event(FixtureKey::Member, "challenge-a", 1_714_124_433).expect("auth"), 253 UnixTimestamp::new(1_714_124_433), 254 ) 255 .expect("author auth"); 256 assert_eq!( 257 relay 258 .handle_event_with_auth(protected.clone(), &auth) 259 .expect("author write"), 260 RelayMessage::Ok { 261 event_id: protected.id().clone(), 262 accepted: true, 263 message: String::new() 264 } 265 ); 266 267 let _ = std::fs::remove_dir_all(root); 268 } 269 270 fn runtime_config(root: &Path) -> BaseRelayRuntimeConfig { 271 parse_base_relay_runtime_config_json( 272 &json!({ 273 "server": { 274 "listen_addr": "127.0.0.1:0", 275 "relay_url": TANGLE_V2_RELAY_URL 276 }, 277 "pocket": { 278 "data_directory": root.join("pocket"), 279 "sync_policy": "flush_on_shutdown", 280 "query": { 281 "allow_scraping": false, 282 "allow_scrape_if_limited_to": 100, 283 "allow_scrape_if_max_seconds": 3600 284 } 285 }, 286 "groups": { 287 "enabled": true, 288 "canonical_relay_url": TANGLE_V2_RELAY_URL, 289 "relay_secret": TANGLE_V2_RELAY_SECRET_HEX, 290 "owner_pubkeys": [FixtureKey::Owner.public_key().as_str()], 291 "admin_pubkeys": [FixtureKey::Admin.public_key().as_str()] 292 }, 293 "auth": { 294 "challenge_ttl_seconds": 300, 295 "created_at_skew_seconds": 600 296 }, 297 "limits": { 298 "max_message_length": 1048576, 299 "max_subid_length": 64, 300 "max_subscriptions_per_connection": 64, 301 "max_filters_per_request": 10, 302 "max_tag_values_per_filter": 100, 303 "max_query_complexity": 2048, 304 "max_limit": 500, 305 "default_limit": 100, 306 "max_event_tags": 200, 307 "max_content_length": 65536, 308 "broadcast_channel_capacity": 16, 309 "per_connection_outbound_queue": 8 310 }, 311 "rate_limits": { 312 "auth": { 313 "per_ip": {"window_seconds": 60, "max_hits": 120}, 314 "per_pubkey": {"window_seconds": 60, "max_hits": 30}, 315 "failures": {"window_seconds": 60, "max_hits": 1}, 316 "failures_per_ip": {"window_seconds": 300, "max_hits": 20} 317 }, 318 "event": { 319 "per_ip": {"window_seconds": 60, "max_hits": 600}, 320 "per_pubkey": {"window_seconds": 60, "max_hits": 120}, 321 "per_kind": {"window_seconds": 60, "max_hits": 1000} 322 }, 323 "group": { 324 "write_per_ip": {"window_seconds": 60, "max_hits": 300}, 325 "write_per_pubkey": {"window_seconds": 60, "max_hits": 60}, 326 "write_per_group": {"window_seconds": 60, "max_hits": 90}, 327 "write_per_kind": {"window_seconds": 60, "max_hits": 300}, 328 "join_flow": {"window_seconds": 300, "max_hits": 10}, 329 "join_flow_per_ip": {"window_seconds": 300, "max_hits": 30} 330 }, 331 "req": { 332 "per_ip": {"window_seconds": 60, "max_hits": 600}, 333 "per_connection": {"window_seconds": 60, "max_hits": 120}, 334 "per_pubkey": {"window_seconds": 60, "max_hits": 240}, 335 "per_group": {"window_seconds": 60, "max_hits": 240}, 336 "per_kind": {"window_seconds": 60, "max_hits": 500}, 337 "broad": {"window_seconds": 60, "max_hits": 30} 338 }, 339 "count": { 340 "per_ip": {"window_seconds": 60, "max_hits": 300}, 341 "per_connection": {"window_seconds": 60, "max_hits": 60}, 342 "per_pubkey": {"window_seconds": 60, "max_hits": 120}, 343 "per_group": {"window_seconds": 60, "max_hits": 120}, 344 "per_kind": {"window_seconds": 60, "max_hits": 240}, 345 "broad": {"window_seconds": 60, "max_hits": 20} 346 } 347 } 348 }) 349 .to_string(), 350 ) 351 .expect("config") 352 } 353 354 fn temp_root(name: &str) -> PathBuf { 355 std::env::temp_dir().join(format!("tangle-ops-{name}-{}", std::process::id())) 356 }