tangle


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

commit a45d9b443f1ff42f99a846c04678b909e46d8850
parent 561fa31180e332da0796f7008d5e1467d1b50711
Author: triesap <tyson@radroots.org>
Date:   Sun, 14 Jun 2026 17:23:24 -0700

protocol: prove hot path coverage

Diffstat:
Mcrates/tangle_bench/src/lib.rs | 16++++++++++++++++
Mcrates/tangle_protocol/src/lib.rs | 176+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/tangle_runtime/src/lib.rs | 28++++++++++++++++++++++++++++
Mcrates/tangle_runtime/src/pocket_conversion.rs | 37+++++++++++++++++++++++++++++++++++++
4 files changed, 257 insertions(+), 0 deletions(-)

diff --git a/crates/tangle_bench/src/lib.rs b/crates/tangle_bench/src/lib.rs @@ -1632,6 +1632,22 @@ mod tests { } #[test] + fn protocol_conversion_for_supported_profile_sizes_is_bounded() { + let dataset = + BenchDataset::generate(BenchDatasetConfig::new(4, 3, 3, 4, 3)).expect("dataset"); + let mut total_event_json_bytes = 0_usize; + for source in dataset.source_events() { + let event_json = tangle_protocol::event_to_value(source.event()).to_string(); + total_event_json_bytes += event_json.len(); + assert!( + tangle_protocol::parse_client_message(&format!("[\"EVENT\",{event_json}]")).is_ok() + ); + } + + assert!(total_event_json_bytes < 1_000_000); + } + + #[test] fn benchmark_profiles_are_explicit_and_unknown_profiles_fail_closed() { assert_eq!( BenchmarkProfileName::all() diff --git a/crates/tangle_protocol/src/lib.rs b/crates/tangle_protocol/src/lib.rs @@ -1681,6 +1681,104 @@ mod tests { } #[test] + fn nip01_client_and_relay_message_conformance_vectors_are_exact() { + let event_payload = event_json("a", "b", 1, tags_json()); + let event = + parse_event_json(&RawEventJson::new(&event_payload).expect("raw")).expect("event"); + let subscription_id = SubscriptionId::new("sub-vector").expect("sub"); + let event_id = EventId::new(&"c".repeat(EventId::HEX_LENGTH)).expect("id"); + let client_vectors = [ + ( + format!("[\"EVENT\",{event_payload}]"), + ClientMessage::Event(event.clone()), + ), + ( + format!("[\"AUTH\",{event_payload}]"), + ClientMessage::Auth(event.clone()), + ), + ( + "[\"REQ\",\"sub-vector\",{\"kinds\":[1]}]".to_owned(), + ClientMessage::Req { + subscription_id: subscription_id.clone(), + filters: vec![ + filter_from_value(&serde_json::json!({"kinds":[1]})).expect("filter"), + ], + }, + ), + ( + "[\"COUNT\",\"sub-vector\",{\"kinds\":[1]}]".to_owned(), + ClientMessage::Count { + subscription_id: subscription_id.clone(), + filters: vec![ + filter_from_value(&serde_json::json!({"kinds":[1]})).expect("filter"), + ], + }, + ), + ( + "[\"CLOSE\",\"sub-vector\"]".to_owned(), + ClientMessage::Close(subscription_id.clone()), + ), + ]; + for (raw, expected) in client_vectors { + assert_eq!(parse_client_message(&raw), Ok(expected)); + } + let relay_vectors = [ + ( + RelayMessage::Event { + subscription_id: subscription_id.clone(), + event: event.clone(), + }, + serde_json::json!(["EVENT", "sub-vector", event_to_value(&event)]), + ), + ( + RelayMessage::Ok { + event_id: event_id.clone(), + accepted: true, + message: String::new(), + }, + serde_json::json!(["OK", event_id.as_str(), true, ""]), + ), + ( + RelayMessage::Eose(subscription_id.clone()), + serde_json::json!(["EOSE", "sub-vector"]), + ), + ( + RelayMessage::Closed { + subscription_id: subscription_id.clone(), + message: "unsupported: search filters are not supported".to_owned(), + }, + serde_json::json!([ + "CLOSED", + "sub-vector", + "unsupported: search filters are not supported" + ]), + ), + ( + RelayMessage::Count { + subscription_id: subscription_id.clone(), + count: 3, + }, + serde_json::json!(["COUNT", "sub-vector", {"count": 3}]), + ), + ( + RelayMessage::Notice("invalid: bad envelope".to_owned()), + serde_json::json!(["NOTICE", "invalid: bad envelope"]), + ), + ( + RelayMessage::Auth("challenge".to_owned()), + serde_json::json!(["AUTH", "challenge"]), + ), + ]; + for (message, expected) in relay_vectors { + assert_eq!(relay_message_to_value(&message), expected); + assert_eq!( + serde_json::from_str::<serde_json::Value>(&message.encode()).expect("encoded"), + expected + ); + } + } + + #[test] fn protocol_conformance_fixtures_cover_dense_envelopes_and_parser_stress() { let event_payload = event_json( "a", @@ -1848,6 +1946,38 @@ mod tests { } #[test] + fn malformed_client_message_corpus_is_rejected_or_parsed_without_panic() { + let valid_event = event_json("a", "b", 1, tags_json()); + let long_sub = "x".repeat(SubscriptionId::MAX_LENGTH + 1); + let oversized_kind = u64::from(u32::MAX) + 1; + let corpus = [ + String::new(), + "[".to_owned(), + "null".to_owned(), + "[null]".to_owned(), + "[\"EVENT\",null]".to_owned(), + format!("[\"EVENT\",{valid_event},{}]", serde_json::json!({})), + "[\"REQ\",{},{}]".to_owned(), + "[\"REQ\",\"sub\",{\"ids\":[]}]".to_owned(), + "[\"REQ\",\"sub\",{\"ids\":[1]}]".to_owned(), + "[\"REQ\",\"sub\",{\"#aa\":[\"value\"]}]".to_owned(), + format!("[\"REQ\",\"sub\",{{\"kinds\":[{oversized_kind}]}}]"), + format!("[\"REQ\",\"{long_sub}\",{{}}]"), + "[\"COUNT\",\"sub\",{\"authors\":[\"BAD\"]}]".to_owned(), + "[\"COUNT\",\"sub\",{\"unknown\":true}]".to_owned(), + "[\"CLOSE\",\"sub\",{}]".to_owned(), + "[\"AUTH\",[]]".to_owned(), + "[\"NOTICE\",\"not a client command\"]".to_owned(), + ]; + for raw in corpus { + std::panic::catch_unwind(|| { + let _ = parse_client_message(&raw); + }) + .expect("parser must not panic"); + } + } + + #[test] fn parse_event_json_rejects_invalid_event_shapes() { assert_eq!( parse_event_json(&RawEventJson::new("{").expect("raw")) @@ -2299,6 +2429,52 @@ mod tests { ); } + #[test] + fn canonical_event_json_preserves_large_tags_without_shape_drift() { + let tags = (0..64) + .map(|index| Tag::from_parts("t", &[&format!("topic-{index}")]).expect("tag")) + .collect::<Vec<_>>(); + let event = unsigned_event(tags, "large tag set"); + let value = + serde_json::from_str::<serde_json::Value>(&event.canonical_json()).expect("json"); + + assert_eq!(value[0], 0); + assert_eq!(value[4].as_array().expect("tags").len(), 64); + assert_eq!(value[5], "large tag set"); + } + + #[test] + fn parser_fuzz_style_scalar_corpus_is_total() { + let ids = ["0", "a", "f", "g", "A", "00", "ff"]; + for id_seed in ids { + let id = id_seed.repeat(EventId::HEX_LENGTH); + let raw = serde_json::json!([ + "EVENT", + { + "id": id, + "pubkey": "1".repeat(PublicKeyHex::HEX_LENGTH), + "created_at": 1_714_124_433_u64, + "kind": 1, + "tags": [["e", "a".repeat(EventId::HEX_LENGTH)]], + "content": "fuzz", + "sig": "2".repeat(SignatureHex::HEX_LENGTH) + } + ]) + .to_string(); + std::panic::catch_unwind(|| { + let _ = parse_client_message(&raw); + }) + .expect("event parser must not panic"); + } + for size in [1_usize, 2, 4, 8, 16, 32, 64, 128] { + let values = (0..size) + .map(|index| serde_json::Value::String(format!("topic-{index}"))) + .collect::<Vec<_>>(); + let raw = serde_json::json!(["REQ", "fuzz", {"#t": values}]).to_string(); + assert!(parse_client_message(&raw).is_ok()); + } + } + fn unsigned_event(tags: Vec<Tag>, content: &str) -> UnsignedEvent { UnsignedEvent::new( PublicKeyHex::new(&"1".repeat(PublicKeyHex::HEX_LENGTH)).expect("pubkey"), diff --git a/crates/tangle_runtime/src/lib.rs b/crates/tangle_runtime/src/lib.rs @@ -68,3 +68,31 @@ pub fn open_tangle_runtime_from_config_path( let config = load_base_relay_runtime_config(path)?; TangleRuntime::open(config).map_err(TangleRuntimeLoadError::OpenRelay) } + +#[cfg(test)] +mod tests { + use crate::pocket_conversion::{pocket_event_to_tangle, tangle_event_to_pocket}; + use tangle_protocol::{Tag, event_from_value, event_to_value}; + use tangle_test_support::{FixtureKey, tangle_v2_event}; + + #[test] + fn pocket_event_conversion_accepts_protocol_event_json_shapes() { + let event = tangle_v2_event( + FixtureKey::Owner, + 1_714_124_433, + 30_402, + vec![ + Tag::from_parts("d", &["market"]).expect("d"), + Tag::from_parts("t", &["radroots", "farm"]).expect("t"), + ], + "json parity", + ) + .expect("event"); + let parsed = event_from_value(&event_to_value(&event)).expect("parsed"); + let pocket = tangle_event_to_pocket(&parsed).expect("pocket"); + let converted = pocket_event_to_tangle(&pocket).expect("converted"); + + assert_eq!(parsed, event); + assert_eq!(converted, event); + } +} diff --git a/crates/tangle_runtime/src/pocket_conversion.rs b/crates/tangle_runtime/src/pocket_conversion.rs @@ -264,4 +264,41 @@ mod tests { assert!(pocket_filter.event_matches(&pocket_event).expect("match")); } + + #[test] + fn pocket_filter_conversion_matches_tangle_filter_matching_for_supported_fields() { + let event = tangle_v2_event( + FixtureKey::Member, + 1_714_124_433, + 1, + vec![ + Tag::from_parts("e", &[&"a".repeat(64)]).expect("e"), + Tag::from_parts("p", &[FixtureKey::Owner.public_key().as_str()]).expect("p"), + Tag::from_parts("t", &["market"]).expect("t"), + ], + "filter parity", + ) + .expect("event"); + let pocket_event = tangle_event_to_pocket(&event).expect("event"); + for value in [ + serde_json::json!({"ids": [event.id().as_str()]}), + serde_json::json!({"authors": [event.unsigned().pubkey().as_str()]}), + serde_json::json!({"kinds": [1]}), + serde_json::json!({"#e": ["a".repeat(64)]}), + serde_json::json!({"#p": [FixtureKey::Owner.public_key().as_str()]}), + serde_json::json!({"#t": ["market"]}), + serde_json::json!({"since": 1_714_124_400, "until": 1_714_124_500}), + serde_json::json!({"limit": 1}), + serde_json::json!({"kinds": [2]}), + serde_json::json!({"#t": ["other"]}), + ] { + let filter = filter_from_value(&value).expect("filter"); + let pocket_filter = tangle_filter_to_pocket(&filter).expect("pocket filter"); + + assert_eq!( + pocket_filter.event_matches(&pocket_event).expect("match"), + filter.matches(&event) + ); + } + } }