tangle


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

commit 88c7733777161c76940c6e1003c6bbc0948404b2
parent 26b081bf8d0929d5343f993fa069a540eb6fb620
Author: triesap <tyson@radroots.org>
Date:   Tue, 16 Jun 2026 13:48:06 -0700

runtime: verify Pocket events natively

Diffstat:
Mcrates/tangle_runtime/src/pocket_event_validation.rs | 89++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Mcrates/tangle_runtime/src/relay/auth.rs | 100+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Mcrates/tangle_runtime/src/relay/core.rs | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
3 files changed, 195 insertions(+), 88 deletions(-)

diff --git a/crates/tangle_runtime/src/pocket_event_validation.rs b/crates/tangle_runtime/src/pocket_event_validation.rs @@ -1,7 +1,6 @@ #![forbid(unsafe_code)] use crate::errors::BaseRelayError; -use std::str; use tangle_protocol::{EventId, Kind, PublicKeyHex, UnixTimestamp}; use tangle_store_pocket::PocketEvent; @@ -60,17 +59,15 @@ pub(crate) fn is_pocket_nip70_protected_event(event: &PocketEvent) -> Result<boo } pub(crate) fn verify_pocket_event_signature(event: &PocketEvent) -> Result<(), BaseRelayError> { - let canonical = pocket_canonical_event_json(event)?; - tangle_crypto::verify_event_signature_bytes( - &canonical, - &event.id().into_inner(), - event.pubkey().as_bytes(), - &event.sig().into_inner(), - ) - .map_err(BaseRelayError::invalid) + event + .verify() + .map_err(|error| BaseRelayError::invalid(error.to_string())) } +#[cfg(test)] pub(crate) fn pocket_canonical_event_json(event: &PocketEvent) -> Result<String, BaseRelayError> { + use std::str; + let tags = event .tags() .map_err(|error| BaseRelayError::invalid(format!("malformed Pocket event tags: {error}")))? @@ -105,45 +102,54 @@ mod tests { pocket_event_pubkey, validate_pocket_event_shape, verify_pocket_event_signature, }; use crate::pocket_conversion::tangle_event_to_pocket; - use tangle_protocol::{Event, EventId, Tag, event_to_value}; - use tangle_store_pocket::parse_pocket_event_json; + use tangle_crypto::RelaySigner; + use tangle_protocol::{Tag, event_to_value}; + use tangle_store_pocket::{ + PocketKind, PocketOwnedEvent, PocketOwnedTags, PocketTime, parse_pocket_event_json, + }; use tangle_test_support::{FixtureKey, tangle_v2_event}; #[test] fn pocket_event_validation_verifies_valid_and_invalid_signatures() { - let event = tangle_v2_event(FixtureKey::Member, 1_714_124_433, 1, Vec::new(), "hello") - .expect("event"); - let pocket = tangle_event_to_pocket(&event).expect("pocket"); + let tags = PocketOwnedTags::empty(); + let pocket = pocket_event(12, 1_714_124_433, 1, &tags, b"hello"); assert_eq!(verify_pocket_event_signature(&pocket), Ok(())); - let signature_source = - tangle_v2_event(FixtureKey::Admin, 1_714_124_433, 1, Vec::new(), "hello") - .expect("signature source"); - let wrong_signature = Event::new( - event.id().clone(), - event.unsigned().clone(), - signature_source.sig().clone(), - ); - let wrong_pocket = tangle_event_to_pocket(&wrong_signature).expect("wrong pocket"); + let signature_source = pocket_event(11, 1_714_124_433, 1, &tags, b"hello"); + let wrong_signature = PocketOwnedEvent::new( + pocket.id(), + pocket.kind(), + pocket.pubkey(), + signature_source.sig(), + pocket.tags().expect("tags"), + pocket.created_at(), + pocket.content(), + ) + .expect("wrong signature"); assert!( - verify_pocket_event_signature(&wrong_pocket) + verify_pocket_event_signature(&wrong_signature) .expect_err("signature") .prefixed_message() .starts_with("invalid:") ); - let wrong_id = Event::new( - EventId::new(&"0".repeat(64)).expect("id"), - event.unsigned().clone(), - event.sig().clone(), - ); - let wrong_id_pocket = tangle_event_to_pocket(&wrong_id).expect("wrong id pocket"); + let id_source = pocket_event(12, 1_714_124_433, 1, &tags, b"other"); + let wrong_id = PocketOwnedEvent::new( + id_source.id(), + pocket.kind(), + pocket.pubkey(), + pocket.sig(), + pocket.tags().expect("tags"), + pocket.created_at(), + pocket.content(), + ) + .expect("wrong id"); assert!( - verify_pocket_event_signature(&wrong_id_pocket) + verify_pocket_event_signature(&wrong_id) .expect_err("id") .prefixed_message() - .starts_with("invalid: event id mismatch:") + .starts_with("invalid:") ); } @@ -198,4 +204,23 @@ mod tests { event.unsigned().canonical_json() ); } + + fn pocket_event( + secret_byte: u8, + created_at: u64, + kind: u16, + tags: &PocketOwnedTags, + content: &[u8], + ) -> PocketOwnedEvent { + let secret = format!("{secret_byte:02x}").repeat(32); + RelaySigner::from_secret_hex(&secret) + .expect("signer") + .sign_pocket_event( + PocketKind::from_u16(kind), + tags, + PocketTime::from_u64(created_at), + content, + ) + .expect("pocket event") + } } diff --git a/crates/tangle_runtime/src/relay/auth.rs b/crates/tangle_runtime/src/relay/auth.rs @@ -316,9 +316,9 @@ fn lower_hex(bytes: &[u8]) -> String { #[cfg(test)] mod tests { use super::{BaseAuthState, generate_auth_challenge}; - use crate::pocket_conversion::tangle_event_to_pocket; use tangle_crypto::RelaySigner; use tangle_protocol::{Event, EventId, Kind, RelayMessage, Tag, UnixTimestamp, UnsignedEvent}; + use tangle_store_pocket::{PocketKind, PocketOwnedEvent, PocketOwnedTags, PocketTime}; #[test] fn auth_state_issues_challenges_and_accepts_multiple_pubkeys() { @@ -597,16 +597,14 @@ mod tests { let mut auth = BaseAuthState::new("wss://relay.radroots.test", 20, 10).expect("auth state"); auth.issue_challenge("challenge-a", UnixTimestamp::new(100)) .expect("challenge"); - let owner = signed_auth_event(7, "challenge-a", 105); - let admin = signed_auth_event(8, "challenge-a", 106); - let owner_pocket = tangle_event_to_pocket(&owner).expect("owner pocket"); - let admin_pocket = tangle_event_to_pocket(&admin).expect("admin pocket"); + let owner = signed_pocket_auth_event(7, "challenge-a", 105); + let admin = signed_pocket_auth_event(8, "challenge-a", 106); let owner_pubkey = auth - .authenticate_pocket(&owner_pocket, UnixTimestamp::new(105)) + .authenticate_pocket(&owner, UnixTimestamp::new(105)) .expect("owner"); let admin_pubkey = auth - .authenticate_pocket(&admin_pocket, UnixTimestamp::new(106)) + .authenticate_pocket(&admin, UnixTimestamp::new(106)) .expect("admin"); assert_ne!(owner_pubkey, admin_pubkey); @@ -620,27 +618,36 @@ mod tests { let mut auth = BaseAuthState::new("wss://relay.radroots.test", 20, 10).expect("auth state"); auth.issue_challenge("challenge-a", UnixTimestamp::new(100)) .expect("challenge"); - let owner = signed_auth_event(7, "challenge-a", 105); - let admin = signed_auth_event(8, "challenge-a", 106); - - let wrong_id = tangle_event_to_pocket(&Event::new( - EventId::new(&"0".repeat(EventId::HEX_LENGTH)).expect("id"), - owner.unsigned().clone(), - owner.sig().clone(), - )) + let owner = signed_pocket_auth_event(7, "challenge-a", 105); + let admin = signed_pocket_auth_event(8, "challenge-a", 105); + + let id_source = signed_pocket_auth_event(7, "challenge-a", 106); + let wrong_id = PocketOwnedEvent::new( + id_source.id(), + owner.kind(), + owner.pubkey(), + owner.sig(), + owner.tags().expect("tags"), + owner.created_at(), + owner.content(), + ) .expect("wrong id pocket"); assert!( auth.authenticate_pocket(&wrong_id, UnixTimestamp::new(105)) .expect_err("id") .prefixed_message() - .starts_with("invalid: event id mismatch:") + .starts_with("invalid:") ); - let wrong_signature = tangle_event_to_pocket(&Event::new( - owner.id().clone(), - owner.unsigned().clone(), - admin.sig().clone(), - )) + let wrong_signature = PocketOwnedEvent::new( + owner.id(), + owner.kind(), + owner.pubkey(), + admin.sig(), + owner.tags().expect("tags"), + owner.created_at(), + owner.content(), + ) .expect("wrong signature pocket"); assert!( auth.authenticate_pocket(&wrong_signature, UnixTimestamp::new(105)) @@ -651,44 +658,43 @@ mod tests { for (event, now, expected) in [ ( - signed_event(9, 1, auth_tags("challenge-a"), 105), + signed_pocket_event(9, 1, pocket_auth_tags("challenge-a"), 105), 105, "invalid: AUTH message must contain kind 22242", ), ( - signed_event( + signed_pocket_event( 9, 22_242, - auth_tags_for("wss://other.radroots.test", "challenge-a"), + pocket_auth_tags_for("wss://other.radroots.test", "challenge-a"), 105, ), 105, "auth-required: auth relay does not match canonical relay URL", ), ( - signed_auth_event(9, "wrong", 105), + signed_pocket_auth_event(9, "wrong", 105), 105, "auth-required: auth challenge does not match", ), ( - signed_auth_event(9, "challenge-a", 121), + signed_pocket_auth_event(9, "challenge-a", 121), 121, "auth-required: auth challenge expired", ), ( - signed_auth_event(9, "challenge-a", 94), + signed_pocket_auth_event(9, "challenge-a", 94), 105, "auth-required: auth event created_at is outside configured skew", ), ( - signed_auth_event(9, "challenge-a", 116), + signed_pocket_auth_event(9, "challenge-a", 116), 105, "auth-required: auth event created_at is outside configured skew", ), ] { - let pocket = tangle_event_to_pocket(&event).expect("pocket"); assert_eq!( - auth.authenticate_pocket(&pocket, UnixTimestamp::new(now)) + auth.authenticate_pocket(&event, UnixTimestamp::new(now)) .expect_err("invalid") .prefixed_message(), expected @@ -711,6 +717,14 @@ mod tests { signed_event(secret_byte, 22_242, auth_tags(challenge), created_at) } + fn signed_pocket_auth_event( + secret_byte: u8, + challenge: &str, + created_at: u64, + ) -> PocketOwnedEvent { + signed_pocket_event(secret_byte, 22_242, pocket_auth_tags(challenge), created_at) + } + fn signed_event(secret_byte: u8, kind: u64, tags: Vec<Tag>, created_at: u64) -> Event { let secret = format!("{:02x}", secret_byte).repeat(32); let signer = RelaySigner::from_secret_hex(&secret).expect("signer"); @@ -724,6 +738,24 @@ mod tests { signer.sign_unsigned_event(unsigned) } + fn signed_pocket_event( + secret_byte: u8, + kind: u16, + tags: PocketOwnedTags, + created_at: u64, + ) -> PocketOwnedEvent { + let secret = format!("{secret_byte:02x}").repeat(32); + RelaySigner::from_secret_hex(&secret) + .expect("signer") + .sign_pocket_event( + PocketKind::from_u16(kind), + &tags, + PocketTime::from_u64(created_at), + b"", + ) + .expect("pocket event") + } + fn auth_tags(challenge: &str) -> Vec<Tag> { auth_tags_for("wss://relay.radroots.test", challenge) } @@ -734,4 +766,12 @@ mod tests { Tag::from_parts("challenge", &[challenge]).expect("challenge"), ] } + + fn pocket_auth_tags(challenge: &str) -> PocketOwnedTags { + pocket_auth_tags_for("wss://relay.radroots.test", challenge) + } + + fn pocket_auth_tags_for(relay: &str, challenge: &str) -> PocketOwnedTags { + PocketOwnedTags::new(&[["relay", relay], ["challenge", challenge]]).expect("tags") + } } diff --git a/crates/tangle_runtime/src/relay/core.rs b/crates/tangle_runtime/src/relay/core.rs @@ -1796,7 +1796,8 @@ mod tests { Tag, UnixTimestamp, UnsignedEvent, filter_from_value, }; use tangle_store_pocket::{ - PocketEvent, PocketOwnedFilter, PocketQueryConfig, PocketStoreConfig, PocketSyncPolicy, + PocketEvent, PocketKind, PocketOwnedEvent, PocketOwnedFilter, PocketOwnedTags, + PocketQueryConfig, PocketStoreConfig, PocketSyncPolicy, PocketTime, }; trait BaseRelayCountTestExt { @@ -2950,24 +2951,22 @@ mod tests { #[test] fn base_relay_pocket_event_path_preserves_event_admission_behavior() { let relay = test_relay("base-relay-pocket-event-store-path", 8); - let valid = signed_public_event(7, 1, Vec::new(), "valid"); - let signature_source = signed_public_event(8, 1, Vec::new(), "signature source"); - let invalid = Event::new( - valid.id().clone(), - valid.unsigned().clone(), - signature_source.sig().clone(), - ); - let ephemeral = signed_public_event(7, 20_001, Vec::new(), "ephemeral"); - let protected = signed_public_event( - 7, - 1, - vec![Tag::from_parts("-", &[]).expect("protected")], - "protected", - ); - let valid_pocket = tangle_event_to_pocket(&valid).expect("valid pocket"); - let invalid_pocket = tangle_event_to_pocket(&invalid).expect("invalid pocket"); - let ephemeral_pocket = tangle_event_to_pocket(&ephemeral).expect("ephemeral pocket"); - let protected_pocket = tangle_event_to_pocket(&protected).expect("protected pocket"); + let tags = PocketOwnedTags::empty(); + let protected_tags = PocketOwnedTags::new(&[["-"]]).expect("protected tags"); + let valid_pocket = signed_pocket_event(7, 1, &tags, b"valid"); + let signature_source = signed_pocket_event(8, 1, &tags, b"valid"); + let invalid_pocket = PocketOwnedEvent::new( + valid_pocket.id(), + valid_pocket.kind(), + valid_pocket.pubkey(), + signature_source.sig(), + valid_pocket.tags().expect("tags"), + valid_pocket.created_at(), + valid_pocket.content(), + ) + .expect("invalid pocket"); + let ephemeral_pocket = signed_pocket_event(7, 20_001, &tags, b"ephemeral"); + let protected_pocket = signed_pocket_event(7, 1, &protected_tags, b"protected"); assert!( rejected_message(relay.handle_pocket_event(&invalid_pocket).expect("invalid")) @@ -2975,27 +2974,27 @@ mod tests { ); assert_eq!(count_kind(&relay, 1), 0); - assert_accepted( + assert_pocket_accepted( relay .handle_pocket_event(&valid_pocket) .expect("valid pocket"), - &valid, + &valid_pocket, ); assert_eq!( relay.handle_pocket_event(&valid_pocket).expect("duplicate"), RelayMessage::Ok { - event_id: valid.id().clone(), + event_id: pocket_event_id(&valid_pocket), accepted: true, message: "duplicate: already have this event".to_owned() } ); assert_eq!(count_kind(&relay, 1), 1); - assert_accepted( + assert_pocket_accepted( relay .handle_pocket_event(&ephemeral_pocket) .expect("ephemeral"), - &ephemeral, + &ephemeral_pocket, ); assert_eq!(count_kind(&relay, 20_001), 0); @@ -3007,11 +3006,11 @@ mod tests { ), "auth-required: protected event requires authenticated event author" ); - assert_accepted( + assert_pocket_accepted( relay .handle_pocket_event_with_auth(&protected_pocket, &authenticated_state(7)) .expect("protected auth"), - &protected, + &protected_pocket, ); } @@ -4274,6 +4273,34 @@ mod tests { signed_event_at(secret_byte, kind, tags, content, 1_714_124_433) } + fn signed_pocket_event( + secret_byte: u8, + kind: u16, + tags: &PocketOwnedTags, + content: &[u8], + ) -> PocketOwnedEvent { + signed_pocket_event_at(secret_byte, kind, tags, content, 1_714_124_433) + } + + fn signed_pocket_event_at( + secret_byte: u8, + kind: u16, + tags: &PocketOwnedTags, + content: &[u8], + created_at: u64, + ) -> PocketOwnedEvent { + let secret = format!("{secret_byte:02x}").repeat(32); + RelaySigner::from_secret_hex(&secret) + .expect("signer") + .sign_pocket_event( + PocketKind::from_u16(kind), + tags, + PocketTime::from_u64(created_at), + content, + ) + .expect("pocket event") + } + fn signed_group_create_event(secret_byte: u8, group_id: &str) -> Event { signed_group_create_event_with_tags(secret_byte, group_id, Vec::new(), 1_714_124_433) } @@ -4324,6 +4351,10 @@ mod tests { signer.sign_unsigned_event(unsigned) } + fn pocket_event_id(event: &PocketEvent) -> EventId { + EventId::new(&event.id().as_hex_string()).expect("event id") + } + fn authenticated_state(secret_byte: u8) -> BaseAuthState { let mut auth = BaseAuthState::new("wss://relay.radroots.test", 60, 600).expect("auth state"); @@ -4429,6 +4460,17 @@ mod tests { ); } + fn assert_pocket_accepted(message: RelayMessage, event: &PocketEvent) { + assert_eq!( + message, + RelayMessage::Ok { + event_id: pocket_event_id(event), + accepted: true, + message: String::new() + } + ); + } + fn rejected_message(message: RelayMessage) -> String { match message { RelayMessage::Ok {