commit 88c7733777161c76940c6e1003c6bbc0948404b2
parent 26b081bf8d0929d5343f993fa069a540eb6fb620
Author: triesap <tyson@radroots.org>
Date: Tue, 16 Jun 2026 13:48:06 -0700
runtime: verify Pocket events natively
Diffstat:
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 {