tangle


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

commit 0a6352ec30b906e2edd615da3fd1ddc28cf4920c
parent 91e88275889d008ecefa9d7cdcb4b261c7a09c57
Author: triesap <tyson@radroots.org>
Date:   Mon, 15 Jun 2026 04:53:42 -0700

protocol: reject duplicate object fields

- Reject duplicate JSON object fields in the retained protocol parser.
- Cover duplicate field errors for client filters and event payloads.
- Prove addressable events preserve repeated and non-indexed tag shapes.
- Validate protocol, runtime, benchmark, workspace, and clippy lanes.

Diffstat:
MCargo.lock | 1+
Mcrates/tangle_protocol/Cargo.toml | 1+
Mcrates/tangle_protocol/src/lib.rs | 181++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
3 files changed, 181 insertions(+), 2 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -1311,6 +1311,7 @@ dependencies = [ name = "tangle_protocol" version = "0.1.0" dependencies = [ + "serde", "serde_json", ] diff --git a/crates/tangle_protocol/Cargo.toml b/crates/tangle_protocol/Cargo.toml @@ -8,6 +8,7 @@ license.workspace = true description = "Nostr protocol types for tangle" [dependencies] +serde = "1" serde_json = "1" [lints] diff --git a/crates/tangle_protocol/src/lib.rs b/crates/tangle_protocol/src/lib.rs @@ -2,6 +2,7 @@ use core::fmt; use core::str::FromStr; +use serde::de::{self, Deserialize, Deserializer, MapAccess, SeqAccess, Visitor}; use std::collections::BTreeMap; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -931,7 +932,7 @@ pub fn filter_from_value(value: &serde_json::Value) -> Result<Filter, String> { } pub fn parse_client_message(raw: &str) -> Result<ClientMessage, String> { - let value: serde_json::Value = serde_json::from_str(raw) + let value = json_value_without_duplicate_fields(raw) .map_err(|source| format!("client message JSON is invalid: {source}"))?; let array = value .as_array() @@ -955,12 +956,118 @@ pub fn parse_client_message(raw: &str) -> Result<ClientMessage, String> { } pub fn parse_event_json(raw: &RawEventJson) -> Result<Event, EventShapeError> { - let value = serde_json::from_str(raw.as_str()).map_err(|source| { + let value = json_value_without_duplicate_fields(raw.as_str()).map_err(|source| { EventShapeError::invalid_field("event", &format!("invalid JSON: {source}")) })?; event_from_value(&value) } +fn json_value_without_duplicate_fields(raw: &str) -> Result<serde_json::Value, serde_json::Error> { + let mut deserializer = serde_json::Deserializer::from_str(raw); + let value = UniqueJsonValue::deserialize(&mut deserializer)?.0; + deserializer.end()?; + Ok(value) +} + +struct UniqueJsonValue(serde_json::Value); + +impl<'de> Deserialize<'de> for UniqueJsonValue { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + deserializer + .deserialize_any(UniqueJsonValueVisitor) + .map(Self) + } +} + +struct UniqueJsonValueVisitor; + +impl<'de> Visitor<'de> for UniqueJsonValueVisitor { + type Value = serde_json::Value; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("a JSON value without duplicate object fields") + } + + fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E> { + Ok(serde_json::Value::Bool(value)) + } + + fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E> { + Ok(serde_json::Value::Number(value.into())) + } + + fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E> { + Ok(serde_json::Value::Number(value.into())) + } + + fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E> + where + E: de::Error, + { + serde_json::Number::from_f64(value) + .map(serde_json::Value::Number) + .ok_or_else(|| E::custom("JSON number must be finite")) + } + + fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> { + Ok(serde_json::Value::String(value.to_owned())) + } + + fn visit_borrowed_str<E>(self, value: &'de str) -> Result<Self::Value, E> { + Ok(serde_json::Value::String(value.to_owned())) + } + + fn visit_string<E>(self, value: String) -> Result<Self::Value, E> { + Ok(serde_json::Value::String(value)) + } + + fn visit_none<E>(self) -> Result<Self::Value, E> { + Ok(serde_json::Value::Null) + } + + fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error> + where + D: Deserializer<'de>, + { + UniqueJsonValue::deserialize(deserializer).map(|value| value.0) + } + + fn visit_unit<E>(self) -> Result<Self::Value, E> { + Ok(serde_json::Value::Null) + } + + fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error> + where + A: SeqAccess<'de>, + { + let mut values = Vec::new(); + while let Some(value) = sequence.next_element::<UniqueJsonValue>()? { + values.push(value.0); + } + Ok(serde_json::Value::Array(values)) + } + + fn visit_map<A>(self, mut object: A) -> Result<Self::Value, A::Error> + where + A: MapAccess<'de>, + { + let mut values = serde_json::Map::new(); + while let Some(field) = object.next_key::<String>()? { + if values.contains_key(&field) { + return Err(de::Error::custom(format!( + "duplicate object field `{field}`" + ))); + } + let value = object.next_value::<UniqueJsonValue>()?; + values.insert(field, value.0); + } + Ok(serde_json::Value::Object(values)) + } +} + pub fn event_from_value(value: &serde_json::Value) -> Result<Event, EventShapeError> { let object = value .as_object() @@ -1681,6 +1788,33 @@ mod tests { } #[test] + fn parser_rejects_duplicate_json_object_fields() { + let duplicate_filter_field = + parse_client_message(r#"["REQ","sub-a",{"limit":1,"limit":2,"kinds":[1]}]"#) + .expect_err("duplicate filter field"); + let duplicate_event_field = parse_event_json( + &RawEventJson::new(&format!( + r#"{{"id":"{}","pubkey":"{}","created_at":1714124433,"kind":1,"tags":[],"content":"one","content":"two","sig":"{}"}}"#, + "a".repeat(EventId::HEX_LENGTH), + "1".repeat(PublicKeyHex::HEX_LENGTH), + "b".repeat(SignatureHex::HEX_LENGTH) + )) + .expect("raw"), + ) + .expect_err("duplicate event field") + .to_string(); + + assert!( + duplicate_filter_field.contains("duplicate object field `limit`"), + "{duplicate_filter_field}" + ); + assert!( + duplicate_event_field.contains("duplicate object field `content`"), + "{duplicate_event_field}" + ); + } + + #[test] fn nip01_client_and_relay_message_conformance_vectors_are_exact() { let event_payload = event_json("a", "b", 1, tags_json()); let event = @@ -1865,6 +1999,49 @@ mod tests { } #[test] + fn parser_preserves_addressable_events_and_weird_tag_shapes() { + let p_tag = "2".repeat(PublicKeyHex::HEX_LENGTH); + let event_payload = format!( + r#"{{"id":"{}","pubkey":"{}","created_at":1714124433,"kind":30023,"tags":[["emoji","🌱","https://example.invalid/seed.png"],["d","market-stall"],["d","shadow"],["é","not-indexed"],["h","Farm","extra"],["p","{}","wss://relay.example","mention"]],"content":"addressable 🌱","sig":"{}"}}"#, + "a".repeat(EventId::HEX_LENGTH), + "1".repeat(PublicKeyHex::HEX_LENGTH), + p_tag, + "b".repeat(SignatureHex::HEX_LENGTH) + ); + let event = + parse_event_json(&RawEventJson::new(&event_payload).expect("raw")).expect("event"); + let coordinate = AddressCoordinate::from_event(&event) + .expect("coordinate") + .expect("addressable"); + let filter = filter_from_value(&serde_json::json!({ + "#d": ["market-stall"], + "#h": ["Farm"], + "#p": [p_tag], + "limit": u64::MAX + })) + .expect("filter"); + + assert_eq!(event.unsigned().tags().len(), 6); + assert_eq!( + event.unsigned().tags()[0].values(), + &[ + "emoji".to_owned(), + "🌱".to_owned(), + "https://example.invalid/seed.png".to_owned() + ] + ); + assert_eq!(event.unsigned().tags()[3].indexed_pair(), None); + assert_eq!(coordinate.d().as_str(), "market-stall"); + assert_eq!(filter.limit(), Some(u64::MAX)); + assert!(filter.matches(&event)); + assert_eq!( + filter_from_value(&serde_json::json!({"#é": ["value"]})) + .expect_err("non ascii tag filter"), + "filter field `#é` is invalid: tag name must be a single ASCII letter" + ); + } + + #[test] fn parse_client_message_rejects_malformed_envelopes() { assert!( parse_client_message("{")