1use serde::{Deserialize, Serialize};
27use serde_json::{Value, json};
28use sha2::{Digest, Sha256};
29
30use crate::nostr_key::{
31 NostrKeyError, schnorr_sign_digest, schnorr_verify_digest, xonly_from_secret,
32};
33
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37pub struct NostrEvent {
38 pub id: String,
40 pub pubkey: String,
42 pub created_at: i64,
44 pub kind: u32,
45 pub tags: Vec<Vec<String>>,
47 pub content: String,
49 pub sig: String,
51}
52
53#[derive(Debug, PartialEq, Eq)]
54pub enum NostrEventError {
55 BadField(&'static str),
57 BadEncoding,
59 IdMismatch,
61 Sig(NostrKeyError),
63 BadContent,
65}
66
67impl std::fmt::Display for NostrEventError {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 match self {
70 NostrEventError::BadField(s) => write!(f, "wire event missing/invalid field: {s}"),
71 NostrEventError::BadEncoding => write!(f, "malformed NIP-01 field encoding"),
72 NostrEventError::IdMismatch => write!(f, "NIP-01 id does not match the event body"),
73 NostrEventError::Sig(e) => write!(f, "NIP-01 signature: {e}"),
74 NostrEventError::BadContent => write!(f, "event content is not a wire event"),
75 }
76 }
77}
78
79pub fn nostr_event_id(
87 pubkey_hex: &str,
88 created_at: i64,
89 kind: u32,
90 tags: &[Vec<String>],
91 content: &str,
92) -> [u8; 32] {
93 let preimage = serde_json::to_string(&json!([0, pubkey_hex, created_at, kind, tags, content]))
94 .expect("a JSON array of scalars/strings always serializes");
95 let mut h = Sha256::new();
96 h.update(preimage.as_bytes());
97 let d = h.finalize();
98 let mut out = [0u8; 32];
99 out.copy_from_slice(&d);
100 out
101}
102
103fn unix_from_rfc3339(ts: &str) -> Option<i64> {
105 time::OffsetDateTime::parse(ts, &time::format_description::well_known::Rfc3339)
106 .ok()
107 .map(|t| t.unix_timestamp())
108}
109
110pub fn wire_to_nostr(
116 wire_event: &Value,
117 nostr_secp_sk: &[u8; 32],
118) -> Result<NostrEvent, NostrEventError> {
119 wire_to_nostr_tagged(wire_event, nostr_secp_sk, &[])
120}
121
122pub fn wire_to_nostr_addressed(
128 wire_event: &Value,
129 nostr_secp_sk: &[u8; 32],
130 peer_xonly_hex: &str,
131) -> Result<NostrEvent, NostrEventError> {
132 let p_tag = vec!["p".to_string(), peer_xonly_hex.to_string()];
133 wire_to_nostr_tagged(wire_event, nostr_secp_sk, std::slice::from_ref(&p_tag))
134}
135
136fn wire_to_nostr_tagged(
140 wire_event: &Value,
141 nostr_secp_sk: &[u8; 32],
142 extra_tags: &[Vec<String>],
143) -> Result<NostrEvent, NostrEventError> {
144 let kind = wire_event
145 .get("kind")
146 .and_then(Value::as_u64)
147 .ok_or(NostrEventError::BadField("kind"))? as u32;
148 let ts = wire_event
149 .get("timestamp")
150 .and_then(Value::as_str)
151 .ok_or(NostrEventError::BadField("timestamp"))?;
152 let created_at = unix_from_rfc3339(ts).ok_or(NostrEventError::BadField("timestamp"))?;
153
154 let xonly = xonly_from_secret(nostr_secp_sk).map_err(NostrEventError::Sig)?;
155 let pubkey_hex = hex::encode(xonly);
156
157 let mut tags: Vec<Vec<String>> = Vec::new();
158 if let Some(from) = wire_event.get("from").and_then(Value::as_str) {
159 tags.push(vec!["wire".to_string(), from.to_string()]);
160 }
161 tags.extend(extra_tags.iter().cloned());
162
163 let content = serde_json::to_string(wire_event).map_err(|_| NostrEventError::BadContent)?;
165
166 let id = nostr_event_id(&pubkey_hex, created_at, kind, &tags, &content);
167 let sig = schnorr_sign_digest(nostr_secp_sk, &id).map_err(NostrEventError::Sig)?;
168
169 Ok(NostrEvent {
170 id: hex::encode(id),
171 pubkey: pubkey_hex,
172 created_at,
173 kind,
174 tags,
175 content,
176 sig: hex::encode(sig),
177 })
178}
179
180pub fn verify_and_decode(ev: &NostrEvent) -> Result<Value, NostrEventError> {
188 verify_transport(ev)?;
189 let wire: Value = serde_json::from_str(&ev.content).map_err(|_| NostrEventError::BadContent)?;
190 if !wire.is_object() {
191 return Err(NostrEventError::BadContent);
192 }
193 Ok(wire)
194}
195
196pub fn verify_transport(ev: &NostrEvent) -> Result<[u8; 32], NostrEventError> {
202 let pubkey = hex_exact::<32>(&ev.pubkey)?;
203 let claimed_id = hex_exact::<32>(&ev.id)?;
204 let sig = hex_exact::<64>(&ev.sig)?;
205 let id = nostr_event_id(&ev.pubkey, ev.created_at, ev.kind, &ev.tags, &ev.content);
206 if id != claimed_id {
207 return Err(NostrEventError::IdMismatch);
208 }
209 schnorr_verify_digest(&pubkey, &id, &sig).map_err(NostrEventError::Sig)?;
210 Ok(pubkey)
211}
212
213fn hex_exact<const N: usize>(s: &str) -> Result<[u8; N], NostrEventError> {
216 let v = hex::decode(s).map_err(|_| NostrEventError::BadEncoding)?;
217 v.as_slice()
218 .try_into()
219 .map_err(|_| NostrEventError::BadEncoding)
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225 use crate::nostr_key::generate_transport_key;
226 use crate::signing::{generate_keypair, sign_message_v31};
227
228 fn signed_wire_event() -> Value {
229 let (sk, pk) = generate_keypair();
230 let msg = json!({
231 "v": "3.1",
232 "timestamp": "2026-06-14T12:00:00Z",
233 "from": "did:wire:slate-lotus-88232017",
234 "to": "did:wire:raven-kettle-1234",
235 "kind": 1,
236 "body": {"content": "hello over nostr"},
237 });
238 sign_message_v31(&msg, &sk, &pk, "slate-lotus").unwrap()
239 }
240
241 #[test]
242 fn id_is_sha256_of_canonical_nip01_array() {
243 let preimage = serde_json::to_string(&json!([
246 0,
247 "ab",
248 1700000000i64,
249 1u32,
250 [["wire", "x"]],
251 "hi"
252 ]))
253 .unwrap();
254 assert_eq!(preimage, r#"[0,"ab",1700000000,1,[["wire","x"]],"hi"]"#);
255 }
256
257 #[test]
258 fn roundtrip_encode_then_verify() {
259 let wire = signed_wire_event();
260 let (nsk, _x) = generate_transport_key();
261 let ev = wire_to_nostr(&wire, &nsk).unwrap();
262 let decoded = verify_and_decode(&ev).unwrap();
264 assert_eq!(decoded, wire, "inner wire event must survive intact");
265 assert_eq!(ev.kind, 1);
267 assert_eq!(
268 ev.created_at,
269 unix_from_rfc3339("2026-06-14T12:00:00Z").unwrap()
270 );
271 assert!(ev.tags.iter().any(|t| t[0] == "wire"));
273 }
274
275 #[test]
276 fn addressed_event_carries_p_tag_and_still_verifies() {
277 let wire = signed_wire_event();
278 let (nsk, _x) = generate_transport_key();
279 let (_psk, peer_x) = generate_transport_key();
280 let peer_hex = hex::encode(peer_x);
281 let ev = wire_to_nostr_addressed(&wire, &nsk, &peer_hex).unwrap();
282 assert!(
284 ev.tags
285 .iter()
286 .any(|t| t.first().map(String::as_str) == Some("p") && t.get(1) == Some(&peer_hex)),
287 "addressed event must carry a [\"p\", <peer>] tag, tags={:?}",
288 ev.tags
289 );
290 assert_eq!(ev.tags[0][0], "wire");
292 assert_eq!(verify_and_decode(&ev).unwrap(), wire);
295 }
296
297 #[test]
298 fn tampered_content_fails_id_check() {
299 let wire = signed_wire_event();
300 let (nsk, _x) = generate_transport_key();
301 let mut ev = wire_to_nostr(&wire, &nsk).unwrap();
302 ev.content.push_str("tampered");
303 assert_eq!(verify_and_decode(&ev), Err(NostrEventError::IdMismatch));
304 }
305
306 #[test]
307 fn tampered_id_with_resigned_body_still_needs_matching_sig() {
308 let wire = signed_wire_event();
311 let (nsk, _x) = generate_transport_key();
312 let mut ev = wire_to_nostr(&wire, &nsk).unwrap();
313 ev.content = serde_json::to_string(&json!({"kind":1,"body":"evil"})).unwrap();
314 ev.id = hex::encode(nostr_event_id(
315 &ev.pubkey,
316 ev.created_at,
317 ev.kind,
318 &ev.tags,
319 &ev.content,
320 ));
321 assert_eq!(
323 verify_and_decode(&ev),
324 Err(NostrEventError::Sig(NostrKeyError::PossessionSig))
325 );
326 }
327
328 #[test]
329 fn forged_pubkey_rejected() {
330 let wire = signed_wire_event();
334 let (nsk, _x) = generate_transport_key();
335 let mut ev = wire_to_nostr(&wire, &nsk).unwrap();
336 let (_other_sk, other_x) = generate_transport_key();
337 ev.pubkey = hex::encode(other_x);
338 ev.id = hex::encode(nostr_event_id(
339 &ev.pubkey,
340 ev.created_at,
341 ev.kind,
342 &ev.tags,
343 &ev.content,
344 ));
345 assert_eq!(
346 verify_and_decode(&ev),
347 Err(NostrEventError::Sig(NostrKeyError::PossessionSig))
348 );
349 }
350
351 #[test]
352 fn missing_kind_or_timestamp_errors() {
353 let (nsk, _x) = generate_transport_key();
354 assert_eq!(
355 wire_to_nostr(&json!({"timestamp":"2026-06-14T12:00:00Z"}), &nsk),
356 Err(NostrEventError::BadField("kind"))
357 );
358 assert_eq!(
359 wire_to_nostr(&json!({"kind":1}), &nsk),
360 Err(NostrEventError::BadField("timestamp"))
361 );
362 }
363
364 #[test]
365 fn nostr_event_serde_roundtrips_relay_shape() {
366 let wire = signed_wire_event();
367 let (nsk, _x) = generate_transport_key();
368 let ev = wire_to_nostr(&wire, &nsk).unwrap();
369 let s = serde_json::to_string(&ev).unwrap();
370 let v: Value = serde_json::from_str(&s).unwrap();
372 for k in [
373 "id",
374 "pubkey",
375 "created_at",
376 "kind",
377 "tags",
378 "content",
379 "sig",
380 ] {
381 assert!(v.get(k).is_some(), "NIP-01 event must carry `{k}`");
382 }
383 let back: NostrEvent = serde_json::from_str(&s).unwrap();
384 assert_eq!(back, ev);
385 }
386}