use std::fmt;
use crate::error::RelayError;
use crate::relay::Event;
#[derive(Debug)]
pub struct VerifyError {
inner: RelayError,
}
impl VerifyError {
pub fn inner(&self) -> &RelayError {
&self.inner
}
pub fn into_inner(self) -> RelayError {
self.inner
}
}
impl fmt::Display for VerifyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "event verification failed: {}", self.inner)
}
}
impl std::error::Error for VerifyError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.inner)
}
}
impl From<RelayError> for VerifyError {
fn from(e: RelayError) -> Self {
Self { inner: e }
}
}
pub struct UncheckedEvent {
event: Event,
}
impl UncheckedEvent {
pub fn new(event: Event) -> Self {
Self { event }
}
pub fn id(&self) -> &str {
&self.event.id
}
pub fn pubkey(&self) -> &str {
&self.event.pubkey
}
pub fn created_at(&self) -> u64 {
self.event.created_at
}
pub fn kind(&self) -> u64 {
self.event.kind
}
pub fn sig(&self) -> &str {
&self.event.sig
}
pub fn verify(self) -> Result<VerifiedEvent, VerifyError> {
self.event.verify()?;
Ok(VerifiedEvent { event: self.event })
}
pub fn into_inner_unchecked(self) -> Event {
self.event
}
}
impl fmt::Debug for UncheckedEvent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("UncheckedEvent")
.field("id", &self.event.id)
.field("pubkey", &self.event.pubkey)
.field("kind", &self.event.kind)
.field("created_at", &self.event.created_at)
.finish_non_exhaustive()
}
}
impl From<Event> for UncheckedEvent {
fn from(event: Event) -> Self {
Self::new(event)
}
}
#[derive(Clone)]
pub struct VerifiedEvent {
event: Event,
}
impl VerifiedEvent {
pub fn id(&self) -> &str {
&self.event.id
}
pub fn pubkey(&self) -> &str {
&self.event.pubkey
}
pub fn created_at(&self) -> u64 {
self.event.created_at
}
pub fn kind(&self) -> u64 {
self.event.kind
}
pub fn sig(&self) -> &str {
&self.event.sig
}
pub fn content(&self) -> &str {
&self.event.content
}
pub fn tags(&self) -> &[Vec<String>] {
&self.event.tags
}
pub fn d_tag(&self) -> Option<&str> {
self.event.d_tag()
}
pub fn get_tag(&self, name: &str) -> Option<&str> {
self.event
.tags
.iter()
.find(|t| t.first().map(|s| s.as_str()) == Some(name))
.and_then(|t| t.get(1).map(|s| s.as_str()))
}
pub fn into_inner(self) -> Event {
self.event
}
pub fn as_inner(&self) -> &Event {
&self.event
}
}
impl fmt::Debug for VerifiedEvent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("VerifiedEvent")
.field("id", &self.event.id)
.field("pubkey", &self.event.pubkey)
.field("kind", &self.event.kind)
.field("created_at", &self.event.created_at)
.field("content", &self.event.content)
.field("tags", &self.event.tags)
.finish()
}
}
impl AsRef<Event> for VerifiedEvent {
fn as_ref(&self) -> &Event {
&self.event
}
}
impl crate::relay::Relay {
pub fn ingest_verified(&self, verified: VerifiedEvent) -> Result<(), RelayError> {
let event = verified.into_inner();
if crate::relay::is_ephemeral(event.kind) {
let _ = self.broadcast(&event);
return Ok(());
}
if crate::relay::is_replaceable(event.kind) {
let target_pubkey = event.pubkey.clone();
let target_kind = event.kind;
let replaced = self.store().replace_where(
&move |e: &Event| e.pubkey == target_pubkey && e.kind == target_kind,
event.clone(),
);
if !replaced {
self.store().put(event.clone());
}
let _ = self.broadcast(&event);
return Ok(());
}
if crate::relay::is_parameterised_replaceable(event.kind) {
let target_pubkey = event.pubkey.clone();
let target_kind = event.kind;
let target_d = event.d_tag().map(|s| s.to_string());
let replaced = self.store().replace_where(
&move |e: &Event| {
e.pubkey == target_pubkey
&& e.kind == target_kind
&& e.d_tag().map(|s| s.to_string()) == target_d
},
event.clone(),
);
if !replaced {
self.store().put(event.clone());
}
let _ = self.broadcast(&event);
return Ok(());
}
self.store().put(event.clone());
let _ = self.broadcast(&event);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::relay::{Event, Relay};
use k256::schnorr::SigningKey;
fn test_sk() -> SigningKey {
SigningKey::from_bytes(&[0x42u8; 32]).expect("valid schnorr key")
}
fn make_signed_event(kind: u64, content: &str, tags: Vec<Vec<String>>) -> Event {
let sk = test_sk();
let pubkey_hex = hex::encode(sk.verifying_key().to_bytes());
let skeleton = Event {
id: String::new(),
pubkey: pubkey_hex.clone(),
created_at: 1_700_000_000,
kind,
tags: tags.clone(),
content: content.to_string(),
sig: String::new(),
};
let id = skeleton.canonical_id();
let id_bytes = hex::decode(&id).unwrap();
let sig = sk.sign_raw(&id_bytes, &[0u8; 32]).expect("schnorr sign_raw");
Event {
id,
pubkey: pubkey_hex,
created_at: 1_700_000_000,
kind,
tags,
content: content.to_string(),
sig: hex::encode(sig.to_bytes()),
}
}
fn make_bad_sig_event() -> Event {
let mut ev = make_signed_event(1, "hello", vec![]);
let mut sig_bytes = hex::decode(&ev.sig).unwrap();
sig_bytes[0] ^= 0x01;
ev.sig = hex::encode(sig_bytes);
ev
}
#[test]
fn unchecked_exposes_routing_metadata() {
let ev = make_signed_event(1, "secret payload", vec![]);
let unchecked = UncheckedEvent::new(ev.clone());
assert_eq!(unchecked.id(), ev.id);
assert_eq!(unchecked.pubkey(), ev.pubkey);
assert_eq!(unchecked.kind(), 1);
assert_eq!(unchecked.created_at(), 1_700_000_000);
assert_eq!(unchecked.sig(), ev.sig);
}
#[test]
fn unchecked_debug_omits_content() {
let ev = make_signed_event(1, "secret payload", vec![vec!["t".into(), "tag".into()]]);
let unchecked = UncheckedEvent::new(ev);
let debug = format!("{:?}", unchecked);
assert!(!debug.contains("secret payload"));
assert!(!debug.contains("tag"));
assert!(debug.contains("UncheckedEvent"));
}
#[test]
fn verify_succeeds_for_valid_event() {
let ev = make_signed_event(1, "hello world", vec![vec!["t".into(), "test".into()]]);
let unchecked = UncheckedEvent::new(ev);
let verified = unchecked.verify().expect("should verify");
assert_eq!(verified.content(), "hello world");
assert_eq!(verified.tags().len(), 1);
assert_eq!(verified.tags()[0], vec!["t", "test"]);
assert_eq!(verified.kind(), 1);
}
#[test]
fn verify_fails_for_bad_signature() {
let ev = make_bad_sig_event();
let unchecked = UncheckedEvent::new(ev);
let err = unchecked.verify().unwrap_err();
assert!(err.to_string().contains("verification failed"));
assert!(matches!(err.inner(), RelayError::BadSignature(_)));
}
#[test]
fn verify_fails_for_tampered_content() {
let mut ev = make_signed_event(1, "original", vec![]);
ev.content = "tampered".to_string();
let unchecked = UncheckedEvent::new(ev);
let err = unchecked.verify().unwrap_err();
assert!(matches!(err.inner(), RelayError::IdMismatch));
}
#[test]
fn verified_content_and_tags() {
let tags = vec![
vec!["e".into(), "abc123".into()],
vec!["p".into(), "def456".into()],
vec!["d".into(), "my-slot".into()],
];
let ev = make_signed_event(30_000, r#"{"name":"alice"}"#, tags);
let verified = UncheckedEvent::new(ev).verify().unwrap();
assert_eq!(verified.content(), r#"{"name":"alice"}"#);
assert_eq!(verified.tags().len(), 3);
assert_eq!(verified.get_tag("e"), Some("abc123"));
assert_eq!(verified.get_tag("p"), Some("def456"));
assert_eq!(verified.get_tag("d"), Some("my-slot"));
assert_eq!(verified.d_tag(), Some("my-slot"));
assert_eq!(verified.get_tag("nonexistent"), None);
}
#[test]
fn verified_into_inner_returns_event() {
let ev = make_signed_event(1, "payload", vec![]);
let original_id = ev.id.clone();
let verified = UncheckedEvent::new(ev).verify().unwrap();
let inner = verified.into_inner();
assert_eq!(inner.id, original_id);
assert_eq!(inner.content, "payload");
}
#[test]
fn verified_as_ref_returns_event() {
let ev = make_signed_event(1, "payload", vec![]);
let verified = UncheckedEvent::new(ev).verify().unwrap();
let inner: &Event = verified.as_ref();
assert_eq!(inner.content, "payload");
}
#[test]
fn verified_clone() {
let ev = make_signed_event(1, "clonable", vec![]);
let verified = UncheckedEvent::new(ev).verify().unwrap();
let cloned = verified.clone();
assert_eq!(cloned.content(), "clonable");
assert_eq!(cloned.id(), verified.id());
}
#[test]
fn from_event_to_unchecked() {
let ev = make_signed_event(1, "test", vec![]);
let unchecked: UncheckedEvent = ev.into();
assert_eq!(unchecked.kind(), 1);
}
#[test]
fn unchecked_escape_hatch() {
let ev = make_signed_event(1, "escape", vec![]);
let original_id = ev.id.clone();
let unchecked = UncheckedEvent::new(ev);
let raw = unchecked.into_inner_unchecked();
assert_eq!(raw.id, original_id);
assert_eq!(raw.content, "escape");
}
#[test]
fn verify_error_display() {
let ev = make_bad_sig_event();
let err = UncheckedEvent::new(ev).verify().unwrap_err();
let display = err.to_string();
assert!(display.starts_with("event verification failed:"));
}
#[test]
fn verify_error_into_inner() {
let ev = make_bad_sig_event();
let err = UncheckedEvent::new(ev).verify().unwrap_err();
let relay_err = err.into_inner();
assert!(matches!(relay_err, RelayError::BadSignature(_)));
}
#[test]
fn relay_ingest_verified_stores_event() {
let relay = Relay::in_memory();
let ev = make_signed_event(1, "verified-ingest", vec![]);
let verified = UncheckedEvent::new(ev.clone()).verify().unwrap();
relay.ingest_verified(verified).unwrap();
let snap = relay.snapshot();
assert_eq!(snap.len(), 1);
assert_eq!(snap[0].id, ev.id);
assert_eq!(snap[0].content, "verified-ingest");
}
#[test]
fn relay_ingest_verified_replaces_nip16() {
let relay = Relay::in_memory();
let a = make_signed_event(0, r#"{"v":1}"#, vec![]);
let b = make_signed_event(0, r#"{"v":2}"#, vec![]);
let va = UncheckedEvent::new(a).verify().unwrap();
let vb = UncheckedEvent::new(b.clone()).verify().unwrap();
relay.ingest_verified(va).unwrap();
relay.ingest_verified(vb).unwrap();
let snap = relay.snapshot();
assert_eq!(snap.len(), 1);
assert_eq!(snap[0].content, r#"{"v":2}"#);
}
#[test]
fn relay_ingest_verified_ephemeral_not_stored() {
let relay = Relay::in_memory();
let mut rx = relay.subscribe();
let ev = make_signed_event(20_001, "ephemeral", vec![]);
let verified = UncheckedEvent::new(ev.clone()).verify().unwrap();
relay.ingest_verified(verified).unwrap();
assert_eq!(relay.snapshot().len(), 0);
let received = rx.try_recv().unwrap();
assert_eq!(received.id, ev.id);
}
#[test]
fn relay_ingest_verified_parameterised_replaceable() {
let relay = Relay::in_memory();
let a = make_signed_event(30_000, "v1", vec![vec!["d".into(), "slot".into()]]);
let b = make_signed_event(30_000, "v2", vec![vec!["d".into(), "slot".into()]]);
let va = UncheckedEvent::new(a).verify().unwrap();
let vb = UncheckedEvent::new(b).verify().unwrap();
relay.ingest_verified(va).unwrap();
relay.ingest_verified(vb).unwrap();
let snap = relay.snapshot();
assert_eq!(snap.len(), 1);
assert_eq!(snap[0].content, "v2");
}
}