use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use sha2::{Digest, Sha256};
use crate::nostr_key::{
NostrKeyError, schnorr_sign_digest, schnorr_verify_digest, xonly_from_secret,
};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NostrEvent {
pub id: String,
pub pubkey: String,
pub created_at: i64,
pub kind: u32,
pub tags: Vec<Vec<String>>,
pub content: String,
pub sig: String,
}
#[derive(Debug, PartialEq, Eq)]
pub enum NostrEventError {
BadField(&'static str),
BadEncoding,
IdMismatch,
Sig(NostrKeyError),
BadContent,
}
impl std::fmt::Display for NostrEventError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
NostrEventError::BadField(s) => write!(f, "wire event missing/invalid field: {s}"),
NostrEventError::BadEncoding => write!(f, "malformed NIP-01 field encoding"),
NostrEventError::IdMismatch => write!(f, "NIP-01 id does not match the event body"),
NostrEventError::Sig(e) => write!(f, "NIP-01 signature: {e}"),
NostrEventError::BadContent => write!(f, "event content is not a wire event"),
}
}
}
pub fn nostr_event_id(
pubkey_hex: &str,
created_at: i64,
kind: u32,
tags: &[Vec<String>],
content: &str,
) -> [u8; 32] {
let preimage = serde_json::to_string(&json!([0, pubkey_hex, created_at, kind, tags, content]))
.expect("a JSON array of scalars/strings always serializes");
let mut h = Sha256::new();
h.update(preimage.as_bytes());
let d = h.finalize();
let mut out = [0u8; 32];
out.copy_from_slice(&d);
out
}
fn unix_from_rfc3339(ts: &str) -> Option<i64> {
time::OffsetDateTime::parse(ts, &time::format_description::well_known::Rfc3339)
.ok()
.map(|t| t.unix_timestamp())
}
pub fn wire_to_nostr(
wire_event: &Value,
nostr_secp_sk: &[u8; 32],
) -> Result<NostrEvent, NostrEventError> {
wire_to_nostr_tagged(wire_event, nostr_secp_sk, &[])
}
pub fn wire_to_nostr_addressed(
wire_event: &Value,
nostr_secp_sk: &[u8; 32],
peer_xonly_hex: &str,
) -> Result<NostrEvent, NostrEventError> {
let p_tag = vec!["p".to_string(), peer_xonly_hex.to_string()];
wire_to_nostr_tagged(wire_event, nostr_secp_sk, std::slice::from_ref(&p_tag))
}
fn wire_to_nostr_tagged(
wire_event: &Value,
nostr_secp_sk: &[u8; 32],
extra_tags: &[Vec<String>],
) -> Result<NostrEvent, NostrEventError> {
let kind = wire_event
.get("kind")
.and_then(Value::as_u64)
.ok_or(NostrEventError::BadField("kind"))? as u32;
let ts = wire_event
.get("timestamp")
.and_then(Value::as_str)
.ok_or(NostrEventError::BadField("timestamp"))?;
let created_at = unix_from_rfc3339(ts).ok_or(NostrEventError::BadField("timestamp"))?;
let xonly = xonly_from_secret(nostr_secp_sk).map_err(NostrEventError::Sig)?;
let pubkey_hex = hex::encode(xonly);
let mut tags: Vec<Vec<String>> = Vec::new();
if let Some(from) = wire_event.get("from").and_then(Value::as_str) {
tags.push(vec!["wire".to_string(), from.to_string()]);
}
tags.extend(extra_tags.iter().cloned());
let content = serde_json::to_string(wire_event).map_err(|_| NostrEventError::BadContent)?;
let id = nostr_event_id(&pubkey_hex, created_at, kind, &tags, &content);
let sig = schnorr_sign_digest(nostr_secp_sk, &id).map_err(NostrEventError::Sig)?;
Ok(NostrEvent {
id: hex::encode(id),
pubkey: pubkey_hex,
created_at,
kind,
tags,
content,
sig: hex::encode(sig),
})
}
pub fn verify_and_decode(ev: &NostrEvent) -> Result<Value, NostrEventError> {
verify_transport(ev)?;
let wire: Value = serde_json::from_str(&ev.content).map_err(|_| NostrEventError::BadContent)?;
if !wire.is_object() {
return Err(NostrEventError::BadContent);
}
Ok(wire)
}
pub fn verify_transport(ev: &NostrEvent) -> Result<[u8; 32], NostrEventError> {
let pubkey = hex_exact::<32>(&ev.pubkey)?;
let claimed_id = hex_exact::<32>(&ev.id)?;
let sig = hex_exact::<64>(&ev.sig)?;
let id = nostr_event_id(&ev.pubkey, ev.created_at, ev.kind, &ev.tags, &ev.content);
if id != claimed_id {
return Err(NostrEventError::IdMismatch);
}
schnorr_verify_digest(&pubkey, &id, &sig).map_err(NostrEventError::Sig)?;
Ok(pubkey)
}
fn hex_exact<const N: usize>(s: &str) -> Result<[u8; N], NostrEventError> {
let v = hex::decode(s).map_err(|_| NostrEventError::BadEncoding)?;
v.as_slice()
.try_into()
.map_err(|_| NostrEventError::BadEncoding)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nostr_key::generate_transport_key;
use crate::signing::{generate_keypair, sign_message_v31};
fn signed_wire_event() -> Value {
let (sk, pk) = generate_keypair();
let msg = json!({
"v": "3.1",
"timestamp": "2026-06-14T12:00:00Z",
"from": "did:wire:slate-lotus-88232017",
"to": "did:wire:raven-kettle-1234",
"kind": 1,
"body": {"content": "hello over nostr"},
});
sign_message_v31(&msg, &sk, &pk, "slate-lotus").unwrap()
}
#[test]
fn id_is_sha256_of_canonical_nip01_array() {
let preimage = serde_json::to_string(&json!([
0,
"ab",
1700000000i64,
1u32,
[["wire", "x"]],
"hi"
]))
.unwrap();
assert_eq!(preimage, r#"[0,"ab",1700000000,1,[["wire","x"]],"hi"]"#);
}
#[test]
fn roundtrip_encode_then_verify() {
let wire = signed_wire_event();
let (nsk, _x) = generate_transport_key();
let ev = wire_to_nostr(&wire, &nsk).unwrap();
let decoded = verify_and_decode(&ev).unwrap();
assert_eq!(decoded, wire, "inner wire event must survive intact");
assert_eq!(ev.kind, 1);
assert_eq!(
ev.created_at,
unix_from_rfc3339("2026-06-14T12:00:00Z").unwrap()
);
assert!(ev.tags.iter().any(|t| t[0] == "wire"));
}
#[test]
fn addressed_event_carries_p_tag_and_still_verifies() {
let wire = signed_wire_event();
let (nsk, _x) = generate_transport_key();
let (_psk, peer_x) = generate_transport_key();
let peer_hex = hex::encode(peer_x);
let ev = wire_to_nostr_addressed(&wire, &nsk, &peer_hex).unwrap();
assert!(
ev.tags
.iter()
.any(|t| t.first().map(String::as_str) == Some("p") && t.get(1) == Some(&peer_hex)),
"addressed event must carry a [\"p\", <peer>] tag, tags={:?}",
ev.tags
);
assert_eq!(ev.tags[0][0], "wire");
assert_eq!(verify_and_decode(&ev).unwrap(), wire);
}
#[test]
fn tampered_content_fails_id_check() {
let wire = signed_wire_event();
let (nsk, _x) = generate_transport_key();
let mut ev = wire_to_nostr(&wire, &nsk).unwrap();
ev.content.push_str("tampered");
assert_eq!(verify_and_decode(&ev), Err(NostrEventError::IdMismatch));
}
#[test]
fn tampered_id_with_resigned_body_still_needs_matching_sig() {
let wire = signed_wire_event();
let (nsk, _x) = generate_transport_key();
let mut ev = wire_to_nostr(&wire, &nsk).unwrap();
ev.content = serde_json::to_string(&json!({"kind":1,"body":"evil"})).unwrap();
ev.id = hex::encode(nostr_event_id(
&ev.pubkey,
ev.created_at,
ev.kind,
&ev.tags,
&ev.content,
));
assert_eq!(
verify_and_decode(&ev),
Err(NostrEventError::Sig(NostrKeyError::PossessionSig))
);
}
#[test]
fn forged_pubkey_rejected() {
let wire = signed_wire_event();
let (nsk, _x) = generate_transport_key();
let mut ev = wire_to_nostr(&wire, &nsk).unwrap();
let (_other_sk, other_x) = generate_transport_key();
ev.pubkey = hex::encode(other_x);
ev.id = hex::encode(nostr_event_id(
&ev.pubkey,
ev.created_at,
ev.kind,
&ev.tags,
&ev.content,
));
assert_eq!(
verify_and_decode(&ev),
Err(NostrEventError::Sig(NostrKeyError::PossessionSig))
);
}
#[test]
fn missing_kind_or_timestamp_errors() {
let (nsk, _x) = generate_transport_key();
assert_eq!(
wire_to_nostr(&json!({"timestamp":"2026-06-14T12:00:00Z"}), &nsk),
Err(NostrEventError::BadField("kind"))
);
assert_eq!(
wire_to_nostr(&json!({"kind":1}), &nsk),
Err(NostrEventError::BadField("timestamp"))
);
}
#[test]
fn nostr_event_serde_roundtrips_relay_shape() {
let wire = signed_wire_event();
let (nsk, _x) = generate_transport_key();
let ev = wire_to_nostr(&wire, &nsk).unwrap();
let s = serde_json::to_string(&ev).unwrap();
let v: Value = serde_json::from_str(&s).unwrap();
for k in [
"id",
"pubkey",
"created_at",
"kind",
"tags",
"content",
"sig",
] {
assert!(v.get(k).is_some(), "NIP-01 event must carry `{k}`");
}
let back: NostrEvent = serde_json::from_str(&s).unwrap();
assert_eq!(back, ev);
}
}