use std::fmt;
use secp256k1::SECP256K1;
use secp256k1::schnorr::Signature;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use super::compute_event_id;
use super::id::EventId;
use super::kind::Kind;
use super::tag::Tags;
use crate::JsonUtil;
use crate::key::PublicKey;
use crate::types::Timestamp;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Error)]
#[non_exhaustive]
pub enum EventError {
#[error("event id does not match the canonical serialization")]
InvalidId,
#[error("event signature verification failed")]
InvalidSignature,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Event {
pub id: EventId,
pub pubkey: PublicKey,
pub created_at: Timestamp,
pub kind: Kind,
pub tags: Tags,
pub content: String,
pub sig: Signature,
}
impl Event {
#[must_use]
pub const fn from_parts(
id: EventId,
pubkey: PublicKey,
created_at: Timestamp,
kind: Kind,
tags: Tags,
content: String,
sig: Signature,
) -> Self {
Self {
id,
pubkey,
created_at,
kind,
tags,
content,
sig,
}
}
#[must_use]
pub fn verify_id(&self) -> bool {
let expected = compute_event_id(
&self.pubkey,
self.created_at,
self.kind,
&self.tags,
&self.content,
);
expected == self.id
}
#[must_use]
pub fn verify_signature(&self) -> bool {
SECP256K1
.verify_schnorr(&self.sig, self.id.as_bytes(), self.pubkey.as_inner())
.is_ok()
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(
level = "debug",
name = "nula.event.verify",
skip(self),
fields(
nostr.event.id = %self.id.to_hex(),
nostr.event.kind = self.kind.as_u16(),
nostr.event.content_size = self.content.len(),
nostr.event.tag_count = self.tags.len(),
),
)
)]
pub fn verify(&self) -> Result<(), EventError> {
if !self.verify_id() {
#[cfg(feature = "tracing")]
tracing::debug!("event id does not match canonical hash");
return Err(EventError::InvalidId);
}
if !self.verify_signature() {
#[cfg(feature = "tracing")]
tracing::debug!("schnorr signature verification failed");
return Err(EventError::InvalidSignature);
}
Ok(())
}
#[must_use]
pub fn is_protected(&self) -> bool {
crate::nips::nip70::is_protected(self)
}
pub fn expiration(&self) -> Result<Option<Timestamp>, crate::nips::nip40::ExpirationError> {
crate::nips::nip40::parse_expiration(self)
}
pub fn is_expired(&self, now: Timestamp) -> Result<bool, crate::nips::nip40::ExpirationError> {
crate::nips::nip40::is_expired(self, now)
}
}
impl fmt::Display for Event {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.try_to_json()
.map_or(Err(fmt::Error), |json| f.write_str(&json))
}
}
#[cfg(test)]
mod tests {
use super::super::tag::Tag;
use super::super::unsigned::UnsignedEvent;
use super::*;
use crate::Keys;
fn fixture_keys() -> Keys {
Keys::parse("0000000000000000000000000000000000000000000000000000000000000003").unwrap()
}
fn signed_event(content: &str) -> Event {
let keys = fixture_keys();
UnsignedEvent::new(
*keys.public_key(),
Timestamp::from_secs(1_700_000_000),
Kind::TEXT_NOTE,
Tags::from_vec(vec![Tag::new(["alt", "test"]).unwrap()]),
content,
)
.sign_with_keys(&keys)
.unwrap()
}
#[test]
fn verify_round_trip() {
let event = signed_event("hello");
event.verify().unwrap();
}
#[test]
fn tampered_id_fails_verify() {
let mut event = signed_event("hello");
let mut bytes = event.id.to_byte_array();
bytes[0] ^= 0xff;
event.id = EventId::from_byte_array(bytes);
assert_eq!(event.verify().unwrap_err(), EventError::InvalidId);
}
#[test]
fn tampered_content_fails_verify() {
let mut event = signed_event("hello");
event.content.push('!');
assert_eq!(event.verify().unwrap_err(), EventError::InvalidId);
}
#[test]
fn forged_signature_fails_verify() {
let mut event = signed_event("hello");
let keys = fixture_keys();
let other_message = [0xaa_u8; 32];
event.sig = keys.sign_schnorr(&other_message);
assert_eq!(event.verify().unwrap_err(), EventError::InvalidSignature);
}
#[test]
fn json_round_trip_preserves_signature() {
let event = signed_event("hello");
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains(r#""kind":1"#));
assert!(json.contains(r#""content":"hello""#));
let parsed: Event = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, event);
parsed.verify().unwrap();
}
#[test]
fn inherent_is_protected_matches_free_function() {
let event = signed_event("public");
assert!(!event.is_protected());
let keys = fixture_keys();
let protected = super::super::EventBuilder::text_note("private")
.created_at(Timestamp::from_secs(1))
.protected()
.sign_with_keys(&keys)
.unwrap();
assert!(protected.is_protected());
}
#[test]
fn inherent_expiration_matches_free_function() {
let keys = fixture_keys();
let event = super::super::EventBuilder::text_note("with-deadline")
.created_at(Timestamp::from_secs(1))
.expiration(Timestamp::from_secs(2_000))
.sign_with_keys(&keys)
.unwrap();
assert_eq!(
event.expiration().unwrap(),
Some(Timestamp::from_secs(2_000))
);
assert!(!event.is_expired(Timestamp::from_secs(1_999)).unwrap());
assert!(event.is_expired(Timestamp::from_secs(2_000)).unwrap());
}
}