tangle


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

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 }